Summary
Two unauthenticated oracle API routes use module-level JavaScript Map objects as TTL caches without enforcing a maximum cardinality, actively deleting expired entries, or applying an eviction policy.
The primary affected route is:
app/app/api/oracle/publishers/route.ts
The route derives its cache key from request-controlled query parameters:
const cacheKey = `${mode}:${feedId || authority || ""}`;
For mode=admin, an unauthenticated requester can supply arbitrary unique authority values:
case "admin":
result = getAdminPublishers(authority);
break;
Every successful result is then written to a module-level cache:
cache.set(cacheKey, {
data: result,
timestamp: Date.now(),
});
The configured five-minute TTL is used only as a freshness check:
const cached = cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
return NextResponse.json(cached.data);
}
When an entry becomes stale, it is ignored but never removed from the Map.
There is currently no:
- Maximum cache cardinality.
- LRU or FIFO eviction policy.
- Periodic expired-entry sweep.
- Stale-entry deletion during cache lookup.
- Explicit maximum
authority length.
- Canonical Solana public-key validation for
authority.
- Cache bypass for the inexpensive local
admin mode.
As a result, unique attacker-controlled cache keys and cached response objects remain strongly referenced by the process after their logical TTL expires. These values cannot be reclaimed by JavaScript garbage collection while the module-level Map remains alive.
A similar unbounded cache pattern also exists in:
app/app/api/oracle/resolve/[ca]/route.ts
The resolver route additionally performs parallel Jupiter and DexScreener requests for unique cache misses, creating a secondary upstream request-amplification condition.
Severity
High — deployment-dependent
Suggested CVSS 3.1 score:
7.5 — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Relevant weakness classifications:
- CWE-400: Uncontrolled Resource Consumption.
- CWE-770: Allocation of Resources Without Limits or Throttling.
The demonstrated security impact is availability degradation or denial of service through persistent process-memory growth.
This report does not claim:
- Private-key disclosure.
- Unauthorized transaction signing.
- Theft of user funds.
- Unauthorized modification of on-chain state.
- Confidentiality impact.
- Integrity impact.
The operational impact depends on the hosting model. Frequently recycled serverless instances may reduce the retention window. Warm serverless instances and long-lived Node.js processes can retain attacker-controlled entries for the lifetime of the process.
Affected Revision
Repository:
dcccrypto/percolator-launch
Branch:
Tested commit:
bb0f7674ee0754b56fc7c5303d97a33173bf8649
Commit subject:
refactor(launch): single shared builder for the registration payload (#2387 fast-follow) (#2399)
The local validation branch was created directly from the tested upstream/playground revision.
No production source files were modified during PoC validation.
Affected Components
Primary affected route
app/app/api/oracle/publishers/route.ts
Secondary affected route
app/app/api/oracle/resolve/[ca]/route.ts
Root Cause Analysis
1. Unbounded module-level cache
The publishers route creates a module-level Map:
const CACHE_TTL_MS = 5 * 60 * 1000;
interface CacheEntry {
data: PublishersResponse;
timestamp: number;
}
const cache = new Map<string, CacheEntry>();
Because this cache is defined at module scope, its contents remain available across requests for the lifetime of the Node.js process or warm serverless instance.
No maximum number of entries is enforced.
2. Request-controlled cache cardinality
The cache key includes query parameters controlled by the requester:
const mode = searchParams.get("mode");
const feedId = searchParams.get("feedId");
const authority = searchParams.get("authority");
const cacheKey = `${mode}:${feedId || authority || ""}`;
For mode=admin, each unique authority creates a distinct key:
admin:<attacker-controlled-value>
An attacker can therefore increase cache cardinality by sending requests containing previously unused authority values.
3. Admin mode writes to the cache without external dependencies
The admin-mode response is generated locally:
case "admin":
result = getAdminPublishers(authority);
break;
The result is subsequently cached:
cache.set(cacheKey, {
data: result,
timestamp: Date.now(),
});
This allocation path does not require:
- Solana RPC.
- Pyth.
- Jupiter.
- DexScreener.
- Helius.
- Any other external HTTP request.
The PoC installs a failing fetch spy and confirms that cache allocation succeeds without invoking fetch.
4. Expired entries are ignored but never deleted
The cache freshness check is:
const cached = cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
return NextResponse.json(cached.data);
}
When the TTL condition evaluates to false, execution continues without removing the stale entry.
The route does not call:
Therefore, the TTL controls whether cached data is returned, but it does not control how long the underlying key and value remain allocated.
5. Attacker-controlled data is retained in both the key and cached value
The admin publisher helper stores the supplied authority inside the response:
publishers: [
{
key: authority,
name: `Authority ${authority.slice(0, 4)}…${authority.slice(-4)}`,
status: "active",
},
],
The supplied value is therefore retained:
- Inside the cache key.
- Inside the cached response object.
The route does not enforce an explicit maximum string length before retaining the value.
It also does not require the value to be a canonical Solana public key before caching it.
Secondary Resolver Cache
The resolver route uses a similar module-level cache:
const cache = new Map<string, CacheEntry>();
const CACHE_TTL_MS = 5 * 60 * 1000;
An entry is returned only while it remains fresh:
const cached = cache.get(ca);
if (cached && Date.now() < cached.expiresAt) {
return NextResponse.json({
...cached.data,
cached: true,
});
}
When the entry expires, it is ignored but not deleted.
Successful unique resolutions are cached without a maximum cardinality:
cache.set(ca, {
data: result,
expiresAt: Date.now() + CACHE_TTL_MS,
});
Unique cache misses also initiate parallel upstream requests:
const [jupResult, dexResult] = await Promise.all([
fetchJupiterPrice(ca),
fetchDexScreenerInfo(ca),
]);
This creates two related resource-consumption risks:
- Persistent growth of the module-level cache.
- Upstream request amplification for unique resolver inputs.
Attack Scenario
-
An unauthenticated requester sends:
/api/oracle/publishers?mode=admin&authority=<unique-value>
-
The unique authority produces a unique cache key.
-
The admin-mode response is generated locally.
-
The result is inserted into the module-level Map.
-
The requester repeats the operation with different authority values.
-
After five minutes, previous entries become logically stale.
-
The stale entries remain physically stored because no deletion or eviction occurs.
-
Subsequent unique requests continue increasing cache cardinality.
-
The Map retains strong references to all stored keys and response objects.
-
JavaScript garbage collection cannot reclaim those objects while they remain referenced.
-
On a sufficiently long-lived instance, sustained accumulation may cause:
- Increased Node.js heap utilization.
- Increased garbage-collection pressure.
- Higher API latency.
- Memory-limit exhaustion.
- Process termination.
- Repeated serverless instance restarts.
- HTTP 5xx responses.
- Partial or complete API unavailability.
Why the Existing TTL Is Insufficient
The current TTL is only a freshness check.
It prevents stale data from being returned, but it does not:
- Remove the expired key.
- Remove the expired response object.
- Reduce cache cardinality.
- Release retained memory.
- Restrict the number of unique entries.
- Prevent persistent growth across requests.
A TTL cache without active deletion or a hard size limit can still grow indefinitely.
Existing Rate Limiting Does Not Remove the Root Cause
The repository applies general API rate limiting, which is useful defense-in-depth.
However, rate limiting does not:
- Enforce maximum cache cardinality.
- Delete expired entries.
- Prevent gradual accumulation over time.
- Prevent distributed-source accumulation.
- Enforce a maximum retained input length.
- Prevent one unique allocation per permitted request.
The cache must independently enforce a hard resource limit.
Safe Proof of Concept
The PoC is implemented as a deterministic Vitest unit test:
app/__tests__/api/oracle-publishers-cache-security.test.ts
The test runs entirely against the locally imported Next.js route handler.
It does not:
- Send traffic to a deployed Percolator service.
- Contact Pyth.
- Contact Jupiter.
- Contact DexScreener.
- Contact Helius.
- Contact Solana RPC.
- Submit blockchain transactions.
- Access user data.
- Perform live load testing.
- Attempt to crash a server.
- Modify production source code.
PoC Methodology
The test performs the following operations:
- Enables Vitest fake timers.
- Freezes the system clock at a deterministic timestamp.
- Stores a reference to the native JavaScript
Map.
- Temporarily replaces the global
Map constructor during route-module initialization.
- Captures the route's private module-level cache without editing production code.
- Restores the native global
Map before requests are created.
- Installs a failing
fetch spy.
- Sends 128 unique
mode=admin requests directly to the local route handler.
- Uses unique 2,048-character authority values.
- Confirms that every request returns HTTP 200.
- Confirms that the response retains the complete supplied authority.
- Identifies the publishers cache through keys beginning with
admin:.
- Confirms that the cache contains 128 entries.
- Advances the mocked clock beyond the configured five-minute TTL.
- Sends one additional unique request.
- Confirms that the cache grows from 128 entries to 129.
- Confirms that the oldest expired entry remains present.
- Confirms that no external
fetch request was performed.
Complete PoC Test
/**
* SECURITY PoC — Oracle publisher cache retains attacker-controlled entries
*
* Safe scope:
* - Runs entirely in Vitest against the local route handler.
* - Does not send traffic to any public deployment.
* - Does not call Pyth, Jupiter, DexScreener, Helius, or Solana RPC.
*
* Vulnerable behavior demonstrated:
* - Unique mode=admin authority values create unique cache entries.
* - Entries remain strongly referenced after the documented 5-minute TTL.
* - Cache cardinality continues growing without a maximum bound.
*/
import { afterEach, describe, expect, it, vi } from "vitest";
import { NextRequest } from "next/server";
type CapturedMap = Map<unknown, unknown>;
function makeRequest(authority: string): NextRequest {
const url = new URL("http://localhost:3000/api/oracle/publishers");
url.searchParams.set("mode", "admin");
url.searchParams.set("authority", authority);
return new NextRequest(url);
}
function uniqueAuthority(index: number, length = 2048): string {
const suffix = index.toString(36).padStart(16, "0");
return `${"A".repeat(Math.max(1, length - suffix.length))}${suffix}`;
}
describe("SECURITY: /api/oracle/publishers unbounded cache retention", () => {
afterEach(() => {
vi.useRealTimers();
vi.unstubAllGlobals();
vi.restoreAllMocks();
vi.resetModules();
});
it("retains every unique attacker-controlled authority after TTL expiry", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-07-14T00:00:00.000Z"));
vi.resetModules();
const NativeMap = globalThis.Map;
const capturedMaps: CapturedMap[] = [];
class CapturingMap<K, V> extends NativeMap<K, V> {
constructor(entries?: readonly (readonly [K, V])[] | null) {
super(entries as Iterable<readonly [K, V]> | undefined);
capturedMaps.push(this as CapturedMap);
}
}
/*
* Capture the route's private module-level Map during module loading.
* Production source code is not modified.
*/
vi.stubGlobal("Map", CapturingMap);
const { GET } = await import("@/app/api/oracle/publishers/route");
/*
* Restore the native Map before creating requests.
* The route cache remains the captured CapturingMap instance.
*/
vi.unstubAllGlobals();
/*
* Prove that the admin-mode allocation path does not require an
* external HTTP request.
*/
const fetchSpy = vi.fn(() => {
throw new Error("Unexpected external network request");
});
vi.stubGlobal("fetch", fetchSpy);
const entryCount = 128;
for (let index = 0; index < entryCount; index += 1) {
const authority = uniqueAuthority(index);
const response = await GET(makeRequest(authority));
expect(response.status).toBe(200);
const body = await response.json();
expect(body.mode).toBe("admin");
expect(body.publishers[0].key).toBe(authority);
}
const oracleCache = capturedMaps.find((candidate) =>
[...candidate.keys()].some(
(key) => typeof key === "string" && key.startsWith("admin:"),
),
);
expect(
oracleCache,
"module-level oracle publishers cache was not captured",
).toBeDefined();
expect(oracleCache!.size).toBe(entryCount);
/*
* Move beyond CACHE_TTL_MS.
*
* The vulnerable implementation rejects stale cache hits,
* but does not delete stale entries or enforce maximum cardinality.
*/
vi.advanceTimersByTime(5 * 60 * 1000 + 1);
const finalAuthority = uniqueAuthority(entryCount);
const finalResponse = await GET(makeRequest(finalAuthority));
expect(finalResponse.status).toBe(200);
expect(oracleCache!.size).toBe(entryCount + 1);
expect(fetchSpy).not.toHaveBeenCalled();
/*
* Demonstrate that the oldest expired attacker-controlled entry
* remains strongly referenced after TTL expiry.
*/
expect(oracleCache!.has(`admin:${uniqueAuthority(0)}`)).toBe(true);
});
});
Reproduction Steps
1. Fetch the latest tested branch state
git fetch upstream playground --prune
2. Check out the tested revision
git switch --detach bb0f7674ee0754b56fc7c5303d97a33173bf8649
3. Place the PoC at
app/__tests__/api/oracle-publishers-cache-security.test.ts
4. Install dependencies when necessary
5. Run only the targeted PoC
CI=1 pnpm --dir app exec vitest run \
__tests__/api/oracle-publishers-cache-security.test.ts \
--reporter=verbose
Observed Result
RUN v4.0.18 C:/Users/PC/percolator-launch/app
✓ __tests__/api/oracle-publishers-cache-security.test.ts
> SECURITY: /api/oracle/publishers unbounded cache retention
> retains every unique attacker-controlled authority after TTL expiry
Test Files 1 passed (1)
Tests 1 passed (1)
Process exit code:
The passing test confirms that:
- Unique admin authorities create unique cache entries.
- 128 unique inputs result in 128 retained entries.
- Advancing time beyond the five-minute TTL does not remove old entries.
- One additional unique request increases cache size to 129.
- The oldest expired entry remains strongly referenced.
- No external HTTP request is required for the allocation path.
- Production source code does not need to be modified to reproduce the issue.
PoC Environment
Operating environment: Windows / Git Bash
Node.js: v26.3.0
pnpm: 10.33.3
Vitest: 4.0.18
Production source status during validation:
Evidence Integrity
PoC SHA-256:
3beadf97f622be1b314cfaaef8655adaf22f5e56e1f672c1e7c860cfa821bd81
Evidence archive SHA-256:
8574cdba5907bb9dc3128f8d33fdf44a928333aa475fdfa545e973439f8a3b67
Evidence files:
oracle-cache-security-evidence.tar.gz
oracle-publishers-cache-security.test.ts
oracle-cache-poc-hardened-output.txt
oracle-cache-poc-metadata.txt
Recommended Remediation
1. Do not cache admin-mode responses
Admin-mode publisher data is generated locally and is inexpensive to calculate.
The safest approach is to return it before entering the shared cache-write path:
case "admin": {
const result = getAdminPublishers(validatedAuthority);
return NextResponse.json(result, {
headers: {
"Cache-Control": "no-store",
},
});
}
Alternatively, explicitly exclude admin mode from cache insertion:
if (mode !== "admin") {
cache.set(cacheKey, {
data: result,
timestamp: Date.now(),
});
}
2. Strictly validate the authority parameter
Before processing mode=admin:
- Require
authority to be present.
- Enforce the expected Solana address length.
- Reject oversized values before constructing the cache key.
- Validate base58 encoding.
- Parse it using
PublicKey.
- Require canonical serialization.
- Return HTTP 400 for invalid values.
Example:
let validatedAuthority: string;
try {
if (!authority) {
throw new Error("Missing authority");
}
const publicKey = new PublicKey(authority);
validatedAuthority = publicKey.toBase58();
if (validatedAuthority !== authority) {
throw new Error("Non-canonical authority");
}
} catch {
return NextResponse.json(
{ error: "Invalid authority" },
{ status: 400 },
);
}
3. Introduce a bounded cache abstraction
Replace the raw module-level Map with a bounded TTL/LRU cache.
The implementation should enforce:
- A fixed maximum number of entries.
- Active expiration.
- LRU or FIFO eviction.
- Removal of stale entries.
- Restricted key length.
- Optional cache-size metrics.
A conservative maximum such as 256 or 512 entries may be appropriate, depending on expected production usage.
4. Delete stale entries during lookup
const cached = cache.get(cacheKey);
if (cached) {
if (Date.now() - cached.timestamp < CACHE_TTL_MS) {
return NextResponse.json(cached.data);
}
cache.delete(cacheKey);
}
This prevents a repeatedly accessed expired key from remaining retained.
Read-time deletion alone is not sufficient because attacker-generated keys may never be requested again. A hard maximum capacity remains required.
5. Sweep expired entries before insertion
const now = Date.now();
for (const [key, entry] of cache) {
if (now - entry.timestamp >= CACHE_TTL_MS) {
cache.delete(key);
}
}
This should be used together with a hard cardinality limit.
6. Enforce maximum cardinality before cache insertion
Example FIFO-style protection:
const MAX_CACHE_ENTRIES = 256;
while (cache.size >= MAX_CACHE_ENTRIES) {
const oldestKey = cache.keys().next().value;
if (oldestKey === undefined) {
break;
}
cache.delete(oldestKey);
}
cache.set(cacheKey, {
data: result,
timestamp: Date.now(),
});
A true LRU implementation would provide better eviction behavior, but any deterministic hard limit is safer than the current unbounded cache.
7. Apply equivalent protections to the resolver route
app/app/api/oracle/resolve/[ca]/route.ts should receive:
- Strict path-parameter validation.
- Maximum parameter length.
- Hard cache-cardinality limit.
- Active expired-entry cleanup.
- LRU or FIFO eviction.
- In-flight request deduplication.
- Bounded negative caching.
- Route-specific rate limiting where appropriate.
- Upstream request concurrency limits.
8. Deduplicate concurrent resolver requests
Concurrent requests for the same unresolved ca should reuse one in-flight promise instead of issuing duplicate Jupiter and DexScreener operations.
Conceptually:
const inFlight = new Map<string, Promise<OracleResolveResult>>();
if (inFlight.has(ca)) {
return inFlight.get(ca);
}
const operation = resolveOracle(ca).finally(() => {
inFlight.delete(ca);
});
inFlight.set(ca, operation);
return operation;
The in-flight map must also be bounded and cleaned reliably.
Recommended Regression Tests
Add tests verifying that:
- Missing admin authority returns HTTP 400.
- Invalid base58 authority returns HTTP 400.
- Oversized authority returns HTTP 400.
- Non-canonical authority returns HTTP 400.
- Admin-mode responses are not cached.
- Expired entries are actively removed.
- Cache cardinality never exceeds the configured maximum.
- Insertion beyond capacity evicts the expected entry.
- The oldest expired entry is no longer retained.
- Resolver cache remains within its configured maximum.
- Concurrent identical resolver requests share one upstream operation.
- Unique resolver inputs cannot create unbounded upstream concurrency.
- Existing valid publisher and resolver responses remain backward-compatible.
Non-Duplication Review
I reviewed open and closed repository issues using terms including:
oracle cache
publishers cache
memory exhaustion
unbounded cache
oracle publishers
resource exhaustion
cache DoS
I did not identify an existing issue specifically describing persistent unbounded cache cardinality in these oracle routes.
The closest previously discussed availability issue involved large getProgramAccounts responses through the RPC proxy.
That issue has a different:
- Endpoint.
- Input path.
- Root cause.
- Resource-consumption mechanism.
- Persistence behavior.
- Upstream dependency.
- Required remediation.
The RPC issue concerns resource consumption caused by one potentially large upstream response.
This report concerns persistent accumulation of independently retained attacker-controlled cache entries across multiple requests.
Testing Safety
No traffic was sent to a public Percolator deployment.
No live denial-of-service testing, stress testing, heap-exhaustion attempt, or destructive operation was performed.
The PoC is a deterministic white-box unit test designed to demonstrate the vulnerable cache state without affecting users or infrastructure.
Suggested Fix Acceptance Criteria
The issue should be considered resolved when all of the following conditions are met:
- Admin-mode responses no longer create cache entries.
- Invalid or oversized authorities are rejected before response construction.
- Both oracle caches enforce a hard maximum cardinality.
- Expired entries are actively removed.
- Resolver requests use bounded concurrency and in-flight deduplication.
- Regression tests confirm that cache size cannot grow beyond its configured maximum.
- Existing oracle publisher and resolver functionality remains operational.
- Targeted tests, type checking, linting, and build validation pass.
Summary
Two unauthenticated oracle API routes use module-level JavaScript
Mapobjects as TTL caches without enforcing a maximum cardinality, actively deleting expired entries, or applying an eviction policy.The primary affected route is:
The route derives its cache key from request-controlled query parameters:
For
mode=admin, an unauthenticated requester can supply arbitrary uniqueauthorityvalues:Every successful result is then written to a module-level cache:
The configured five-minute TTL is used only as a freshness check:
When an entry becomes stale, it is ignored but never removed from the
Map.There is currently no:
authoritylength.authority.adminmode.As a result, unique attacker-controlled cache keys and cached response objects remain strongly referenced by the process after their logical TTL expires. These values cannot be reclaimed by JavaScript garbage collection while the module-level
Mapremains alive.A similar unbounded cache pattern also exists in:
The resolver route additionally performs parallel Jupiter and DexScreener requests for unique cache misses, creating a secondary upstream request-amplification condition.
Severity
High — deployment-dependent
Suggested CVSS 3.1 score:
Relevant weakness classifications:
The demonstrated security impact is availability degradation or denial of service through persistent process-memory growth.
This report does not claim:
The operational impact depends on the hosting model. Frequently recycled serverless instances may reduce the retention window. Warm serverless instances and long-lived Node.js processes can retain attacker-controlled entries for the lifetime of the process.
Affected Revision
Repository:
Branch:
Tested commit:
Commit subject:
The local validation branch was created directly from the tested
upstream/playgroundrevision.No production source files were modified during PoC validation.
Affected Components
Primary affected route
Secondary affected route
Root Cause Analysis
1. Unbounded module-level cache
The publishers route creates a module-level
Map:Because this cache is defined at module scope, its contents remain available across requests for the lifetime of the Node.js process or warm serverless instance.
No maximum number of entries is enforced.
2. Request-controlled cache cardinality
The cache key includes query parameters controlled by the requester:
For
mode=admin, each uniqueauthoritycreates a distinct key:An attacker can therefore increase cache cardinality by sending requests containing previously unused authority values.
3. Admin mode writes to the cache without external dependencies
The admin-mode response is generated locally:
The result is subsequently cached:
This allocation path does not require:
The PoC installs a failing
fetchspy and confirms that cache allocation succeeds without invokingfetch.4. Expired entries are ignored but never deleted
The cache freshness check is:
When the TTL condition evaluates to false, execution continues without removing the stale entry.
The route does not call:
Therefore, the TTL controls whether cached data is returned, but it does not control how long the underlying key and value remain allocated.
5. Attacker-controlled data is retained in both the key and cached value
The admin publisher helper stores the supplied authority inside the response:
The supplied value is therefore retained:
The route does not enforce an explicit maximum string length before retaining the value.
It also does not require the value to be a canonical Solana public key before caching it.
Secondary Resolver Cache
The resolver route uses a similar module-level cache:
An entry is returned only while it remains fresh:
When the entry expires, it is ignored but not deleted.
Successful unique resolutions are cached without a maximum cardinality:
Unique cache misses also initiate parallel upstream requests:
This creates two related resource-consumption risks:
Attack Scenario
An unauthenticated requester sends:
The unique
authorityproduces a unique cache key.The admin-mode response is generated locally.
The result is inserted into the module-level
Map.The requester repeats the operation with different authority values.
After five minutes, previous entries become logically stale.
The stale entries remain physically stored because no deletion or eviction occurs.
Subsequent unique requests continue increasing cache cardinality.
The
Mapretains strong references to all stored keys and response objects.JavaScript garbage collection cannot reclaim those objects while they remain referenced.
On a sufficiently long-lived instance, sustained accumulation may cause:
Why the Existing TTL Is Insufficient
The current TTL is only a freshness check.
It prevents stale data from being returned, but it does not:
A TTL cache without active deletion or a hard size limit can still grow indefinitely.
Existing Rate Limiting Does Not Remove the Root Cause
The repository applies general API rate limiting, which is useful defense-in-depth.
However, rate limiting does not:
The cache must independently enforce a hard resource limit.
Safe Proof of Concept
The PoC is implemented as a deterministic Vitest unit test:
The test runs entirely against the locally imported Next.js route handler.
It does not:
PoC Methodology
The test performs the following operations:
Map.Mapconstructor during route-module initialization.Mapbefore requests are created.fetchspy.mode=adminrequests directly to the local route handler.admin:.fetchrequest was performed.Complete PoC Test
Reproduction Steps
1. Fetch the latest tested branch state
2. Check out the tested revision
3. Place the PoC at
4. Install dependencies when necessary
5. Run only the targeted PoC
CI=1 pnpm --dir app exec vitest run \ __tests__/api/oracle-publishers-cache-security.test.ts \ --reporter=verboseObserved Result
Process exit code:
The passing test confirms that:
PoC Environment
Production source status during validation:
Evidence Integrity
PoC SHA-256:
Evidence archive SHA-256:
Evidence files:
Recommended Remediation
1. Do not cache admin-mode responses
Admin-mode publisher data is generated locally and is inexpensive to calculate.
The safest approach is to return it before entering the shared cache-write path:
Alternatively, explicitly exclude admin mode from cache insertion:
2. Strictly validate the authority parameter
Before processing
mode=admin:authorityto be present.PublicKey.Example:
3. Introduce a bounded cache abstraction
Replace the raw module-level
Mapwith a bounded TTL/LRU cache.The implementation should enforce:
A conservative maximum such as 256 or 512 entries may be appropriate, depending on expected production usage.
4. Delete stale entries during lookup
This prevents a repeatedly accessed expired key from remaining retained.
Read-time deletion alone is not sufficient because attacker-generated keys may never be requested again. A hard maximum capacity remains required.
5. Sweep expired entries before insertion
This should be used together with a hard cardinality limit.
6. Enforce maximum cardinality before cache insertion
Example FIFO-style protection:
A true LRU implementation would provide better eviction behavior, but any deterministic hard limit is safer than the current unbounded cache.
7. Apply equivalent protections to the resolver route
app/app/api/oracle/resolve/[ca]/route.tsshould receive:8. Deduplicate concurrent resolver requests
Concurrent requests for the same unresolved
cashould reuse one in-flight promise instead of issuing duplicate Jupiter and DexScreener operations.Conceptually:
The in-flight map must also be bounded and cleaned reliably.
Recommended Regression Tests
Add tests verifying that:
Non-Duplication Review
I reviewed open and closed repository issues using terms including:
I did not identify an existing issue specifically describing persistent unbounded cache cardinality in these oracle routes.
The closest previously discussed availability issue involved large
getProgramAccountsresponses through the RPC proxy.That issue has a different:
The RPC issue concerns resource consumption caused by one potentially large upstream response.
This report concerns persistent accumulation of independently retained attacker-controlled cache entries across multiple requests.
Testing Safety
No traffic was sent to a public Percolator deployment.
No live denial-of-service testing, stress testing, heap-exhaustion attempt, or destructive operation was performed.
The PoC is a deterministic white-box unit test designed to demonstrate the vulnerable cache state without affecting users or infrastructure.
Suggested Fix Acceptance Criteria
The issue should be considered resolved when all of the following conditions are met: