
Cloud Vulnerability DB
A community-led vulnerabilities database
OpenNext stores "use cache" entries under a storage key derived from the raw cache key provided by Next.js. That key embeds the serialized arguments of the cached function. The S3 incremental caches (s3, the default, s3-lite, and the S3 part of multi-tier-ddb-s3) and fs-dev build the object key with path.join, which normalizes path segments. As a result, different argument values can map to the same stored entry. An attacker who can pass a string to a "use cache" function can then have their result served to users calling it with another value.
With enough ../ segments, the S3 object key also escapes the build ID and the configured CACHE_BUCKET_KEY_PREFIX. s3-lite and multi-tier-ddb-s3 also send the key as a URL path, so the .composable suffix can be escaped too.
@opennextjs/cloudflare bundles the same "use cache" handler and is affected through its regional cache and, to a lesser extent, its KV/R2 incremental caches.
For a cached function getPost(slug), the Next.js cache key looks like ["<buildId>","<functionId>",["<slug>"]]. The S3 key is built as:
path.posix.join(CACHE_BUCKET_KEY_PREFIX, BUILD_ID, `${key}.composable`)path.posix.join collapses //, removes ./ and resolves ../:
| Argument | S3 object key |
|---|---|
a/b | cache-prefix/BUILD_ID/["BUILD_ID","fnA",["a/b"]].composable |
a//b | same |
a/./b | same |
a/x/../b | same |
p/../../../../x (function fnA) | ../x"]].composable |
q/../../../../x (function fnB) | same (different function, prefix and build ID dropped) |
Request-controlled strings can contain these sequences: search params, or route params containing an encoded %2F. So the first request to compute a value decides what is stored for every argument value that normalizes the same way. | |
s3-lite and the S3 part of multi-tier-ddb-s3 then send that key as the path of a URL (https://<bucket>.s3.<region>.amazonaws.com/<key>), which transforms it further: | |
- # truncates the key, and ? starts the query string. | |
- %XX sequences are decoded. | |
- \ becomes /. | |
So the .composable suffix can be escaped and other entry types targeted. For example, the argument p/../index.cache# writes to <prefix>/<buildId>/index.cache, the ISR entry for /. | |
The raw key is also used as the path in the original-mode tag caches (dynamodb, dynamodb-lite and fs-dev), which build their keys with path.join too. | |
@opennextjs/cloudflare bundles the @opennextjs/aws "use cache" handler, so it is affected too: | |
- The regional cache (withRegionalCache) builds a Cache API URL as http://cache.local/<buildId>/<key>.composable, so different arguments can share the same regional cache entry: | |
- The URL builder collapses // with a replace. | |
- URL parsing resolves ./ and ../, \ becomes /, and characters are percent-encoded, so a b and a%20b collide. | |
- The Cache API ignores everything after #. | |
- The KV and R2 incremental caches store the SHA-256 of the UTF-8 encoded key. UTF-8 encoding replaces lone surrogates with U+FFFD, so keys that differ only by lone surrogates collide. String arguments are JSON-escaped in the key (\ud800). Raw lone surrogates only occur with binary arguments (ArrayBuffer, typed arrays, Blob). | |
| All of these are fixed by the same change: the handler now passes a fixed-format key to every backend. |
An application is affected when all of the following are true:
"use cache" and is deployed with either:@opennextjs/aws with the S3 (s3, the default, or s3-lite) or multi-tier-ddb-s3 incremental cache, or@opennextjs/cloudflare with the regional cache (key normalization) or the KV/R2 incremental cache (lone surrogates in binary arguments only)."use cache" function receives a request-controlled string argument that is not validated or encoded, or, for KV/R2, a request-controlled binary argument.s3, the written key always ends with the serialized end of the arguments plus .composable, so overwriting an arbitrary existing entry has not been demonstrated. With s3-lite and multi-tier-ddb-s3, the suffix can be escaped (see Details), so an attacker can overwrite existing entries of other types, such as ISR pages. Those entries hold a composable value, so they are read as a miss and regenerated: the impact is integrity and availability, not content injection.Medium (CVSS 3.1: 4.8)
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:L
Attack complexity is rated High because exploitation depends on how the application uses request input as "use cache" arguments.
The patched version stores "use cache" entries, in both the incremental cache and the tag cache, under the SHA-256 of the exact UTF-16 code units of the Next.js cache key. Every backend receives a fixed-format hex key. Existing "use cache" entries are recomputed once after the upgrade.
@opennextjs/cloudflare is fixed by a release that bumps its @opennextjs/aws dependency to the patched version.
Validate request-controlled "use cache" arguments, or encode them before the call so that they cannot contain / (for example lookup(encodeURIComponent(p))), and decode inside the function if needed. This does not cover the lone surrogate collisions on KV/R2 from binary arguments.
On @opennextjs/cloudflare, not wrapping the incremental cache with withRegionalCache removes the path normalization (KV/R2 alone are only affected by lone surrogates).
Source: NVD
Free Vulnerability Assessment
Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.
Get a personalized demo
"Best User Experience I have ever seen, provides full visibility to cloud workloads."
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
"We know that if Wiz identifies something as critical, it actually is."