Classification
| Field |
Value |
| Category |
Logic error; missing error handling; runtime type validation |
| Affected endpoint |
POST /api/oracle/set-price-cap |
| Privilege required |
Valid administrative secret |
| Audited branch |
playground |
| Audited commit |
41bb1304705cd7652b49ef7a4303454d1646415c |
| Validation date |
2026-08-12 |
Executive summary
The administrative price-cap route deliberately interprets an empty request body as an instruction to update every admin-oracle market controlled by the configured crank keypair. However, it implements that contract by initializing body to {} and swallowing every exception raised by req.json().
As a result, an authenticated request containing non-empty but malformed or truncated JSON is indistinguishable from the intentionally empty-body command. The parse failure leaves body as {}, the route selects the default cap, omits the single-market target, queries all matching markets, and can submit one on-chain transaction per validated market.
This is a fail-open scope expansion in an administrative write endpoint: invalid input can expand the operation from the intended request to a batch affecting all eligible markets. This report does not claim an authentication bypass. The issue is reachable only after the existing administrative authentication and crank-key configuration checks succeed.
Affected code
Security and correctness invariant
A request that fails parsing or runtime validation must fail closed. It must never broaden an administrative operation from a specific or invalid request to an all-target batch operation.
The route violates this invariant because the parse-error state and the intentional all-market state are represented by the same value: {}.
Root cause
The vulnerable control flow is:
let body: { slabAddress?: string; maxChangeE2bps?: number } = {};
try {
body = await req.json();
} catch {
// empty body is valid — means "all admin-oracle markets"
}
The catch block does not establish that the body is actually empty. It also catches malformed JSON, truncated JSON, unsupported encodings surfaced by the parser, and any other exception from req.json(). After the exception, execution continues with body === {}.
The next decisions compound the problem:
body.maxChangeE2bps is absent, so the route assigns DEFAULT_MAX_CHANGE_E2BPS (1_000n).
body.slabAddress is absent, so the route selects the all-market branch.
- The route queries the
markets table for rows whose oracle_authority matches the crank public key.
- It validates candidate accounts on-chain, retaining at most 50 addresses.
- It sends a separate
SetOraclePriceCap transaction for every retained address.
The on-chain ownership checks are valuable, but they validate which accounts are eligible; they do not validate that the caller intended an all-market operation. Consequently, they do not mitigate the scope-confusion bug.
There are two additional runtime-validation symptoms. JSON null is valid and therefore does not enter the parser catch. The static TypeScript annotation is erased at runtime, so the next access to body.maxChangeE2bps throws a TypeError. JSON arrays also bypass object-shape validation; an empty array has neither property and therefore selects the same default-cap/all-market branch as {}. This independently confirms that the route is relying on a compile-time annotation instead of validating the runtime JSON shape.
Preconditions and reachability
All of the following are required for the batch-write impact:
- The caller supplies a valid
x-admin-secret accepted by checkAdminSecret.
CRANK_KEYPAIR is configured and parses successfully.
- The request contains malformed/truncated non-empty JSON.
- The database returns at least one market whose
oracle_authority matches the crank public key.
- At least one returned slab is a valid public key and is owned by the configured Percolator program.
These preconditions limit attacker reachability, but they do not eliminate operational risk. An internal client bug, a partially written request body, an automation error, or transport truncation can satisfy the malformed-body condition. Administrative endpoints should be especially strict because an input failure must not be interpreted as a broader write instruction.
Deterministic PoC
The following safe local harness uses the same WHATWG Request.json() primitive and mirrors the route's parse/default/target decisions. It does not connect to Solana, Supabase, or the production application.
async function parseCurrentRouteStyle(rawBody) {
let body = {};
let parseRejected = false;
try {
body = await new Request("http://local.test/api/oracle/set-price-cap", {
method: "POST",
headers: { "content-type": "application/json" },
body: rawBody,
}).json();
} catch {
parseRejected = true;
}
let outcome;
try {
const maxChangeE2bps =
body.maxChangeE2bps == null ? 1000n : BigInt(body.maxChangeE2bps);
const target = body.slabAddress
? "single-market"
: "all-admin-oracle-markets";
outcome = { maxChangeE2bps: maxChangeE2bps.toString(), target };
} catch (error) {
outcome = { throws: error.constructor.name };
}
return { rawBody, parseRejected, body, outcome };
}
(async () => {
for (const raw of ["", "{", "null", "[]", "{}", '{"slabAddress":"S"}']) {
console.log(JSON.stringify(await parseCurrentRouteStyle(raw)));
}
})();
Observed output on the audited snapshot's runtime environment:
{"rawBody":"","parseRejected":true,"body":{},"outcome":{"maxChangeE2bps":"1000","target":"all-admin-oracle-markets"}}
{"rawBody":"{","parseRejected":true,"body":{},"outcome":{"maxChangeE2bps":"1000","target":"all-admin-oracle-markets"}}
{"rawBody":"null","parseRejected":false,"body":null,"outcome":{"throws":"TypeError"}}
{"rawBody":"[]","parseRejected":false,"body":[],"outcome":{"maxChangeE2bps":"1000","target":"all-admin-oracle-markets"}}
{"rawBody":"{}","parseRejected":false,"body":{},"outcome":{"maxChangeE2bps":"1000","target":"all-admin-oracle-markets"}}
{"rawBody":"{\"slabAddress\":\"S\"}","parseRejected":false,"body":{"slabAddress":"S"},"outcome":{"maxChangeE2bps":"1000","target":"single-market"}}
The complete source-to-sink path is therefore:
authenticated malformed request
-> Request.json() rejects
-> catch suppresses the parse error
-> body remains {}
-> default maxChangeE2bps = 1_000n
-> slabAddress is absent
-> query all matching admin-oracle markets
-> validate up to 50 slabs
-> sendAndConfirmTransaction once per retained slab
Actual behavior
- Malformed non-empty JSON is accepted as the semantic equivalent of an intentional empty-body all-market command.
- The default cap is selected even though no valid request document supplied that value.
- Up to 50 eligible markets can be processed in sequence.
- A valid JSON
null body reaches an unhandled property access and produces a server error instead of a validation response.
- A valid JSON array such as
[] receives the default cap and enters the all-market branch rather than returning 400.
- The successful response does not reveal that the original JSON document failed to parse.
Expected behavior
- Malformed JSON must return
400 Bad Request before any database query, account lookup, signing, or transaction submission.
- Parsed JSON must be validated as a non-null, non-array object.
- An all-market administrative operation must require an explicit, positively validated intent marker that cannot be produced by a parse failure.
- Invalid runtime shapes such as
null, arrays, strings, and numbers must return a stable 400 response.
Impact
Integrity impact
An authenticated malformed request can overwrite previously configured circuit-breaker values with the route's default value across every eligible market. Markets with intentionally different caps may therefore receive the wrong configuration.
Operational impact
The transaction loop is sequential and records per-market success or failure. If some transactions land before a later transaction fails, the system can enter a partially updated state. Recovery requires determining which transactions landed and restoring the intended cap for each affected market.
Availability and cost impact
One malformed request can initiate database work, an on-chain multi-account lookup, and up to 50 signed transaction attempts. This is bounded by MAX_SLAB_BATCH, but it is still materially more work than rejecting an invalid request at the parser boundary.
Severity rationale
Severity is Low after conservative recalibration:
| Factor |
Assessment |
| Impact |
Can change an on-chain circuit-breaker parameter on multiple eligible markets, but the fallback value is the route's deliberately protective default rather than an attacker-selected arbitrary value |
| Likelihood |
Requires a valid administrative secret, a configured crank signer, eligible markets, and an authenticated request whose body is malformed or has the wrong runtime shape |
| Reach |
Bounded to at most 50 markets whose database authority matches the crank and whose accounts pass the on-chain ownership check |
| Detectability |
The route logs discovered markets and each transaction result, and returns per-market results to the caller |
| Recovery |
Requires identifying landed transactions and reapplying intended caps; inconvenient but normally reversible by the authorized operator |
The issue is therefore a real fail-open administrative input bug, but its trigger is uncommon and privileged. There is no authentication bypass, crank-key compromise, arbitrary account targeting, or unprivileged attack path in this report. PR #2494 independently characterizes invalid input on this endpoint as an admin-only malformed-request problem rather than an attack; the present bug has broader scope because it can reach multiple markets, but that historical calibration supports Low rather than Medium.
Recommended fix
The safest contract is to remove “empty body means all” and require explicit scope:
{ "slabAddress": "...", "maxChangeE2bps": "1000" }
or:
{ "all": true, "maxChangeE2bps": "1000" }
Recommended implementation properties:
- Parse the body inside a dedicated error boundary and return
400 for every parse failure.
- Validate the parsed value as
typeof value === "object", value !== null, and !Array.isArray(value).
- Validate an explicit discriminated scope: exactly one of
slabAddress or all: true.
- Reject
{}, null, arrays, primitive JSON values, and unknown contradictory scope fields.
- Retain the existing administrative authentication, u64 validation, batch-size cap, and on-chain ownership validation.
- Do not query Supabase or construct transactions until the full request object has passed validation.
If backward compatibility absolutely requires an empty body, read await req.text() first. Only a truly empty or whitespace-only string should enter that legacy branch; a non-empty string that fails JSON.parse must return 400. Even then, explicit { "all": true } is preferable because it makes destructive scope reviewable in logs and client code.
Suggested regression tests
- Valid auth + malformed JSON (
"{") returns 400; Supabase and sendAndConfirmTransaction are not called.
- Valid auth + truncated JSON returns
400 and performs no side effects.
- Valid auth + JSON
null returns 400.
- Valid auth + JSON array returns
400.
- Valid auth + primitive string/number/boolean returns
400.
- Empty object returns
400 when explicit scope is adopted.
{ "all": true } is the only payload that enters the all-market branch.
{ "slabAddress": valid } processes exactly one slab.
- Supplying both
all and slabAddress returns 400.
- Invalid
maxChangeE2bps returns 400 before target discovery.
- A batch with a later transaction failure reports partial results without obscuring which markets were changed.
Existing test gap
The current oracle-set-price-cap.test.ts coverage verifies authentication failures and a non-integer maxChangeE2bps. It does not cover malformed JSON, valid non-object JSON, or an explicit distinction between single-market and all-market intent. The separate u64-bound test addresses numeric range handling, not parse-failure scope expansion.
Duplicate analysis
Searches were performed against open issues, closed issues, pull requests, and commits using exact and broad variants including set-price-cap, malformed JSON, invalid JSON, empty body, and all admin-oracle markets.
Related work is materially different:
- Issue #2078 is a closed, repository-wide input-validation audit that marked body parsing and this route as complete; it does not identify malformed/non-object JSON selecting the all-market branch. The new PoC demonstrates a boundary that the checklist missed, so the audit conclusion is context rather than a duplicate finding.
- Issue #1946 covers negative or unbounded
maxChangeE2bps validation.
- Issue #2231 covers on-chain ownership validation for slab addresses sourced from Supabase.
- PR #2494 bounds the numeric
maxChangeE2bps path to u64.
- PR #1694 introduced/hardened the price-cap endpoint and its authentication/price safeguards.
None of those items covers the parse-error-to-all-market control-flow transition described here. No matching issue or PR was found for malformed/invalid JSON being interpreted as an all-market operation as of 2026-08-12.
Validation limitations
The repository snapshot did not contain installed dependencies, so the complete Next.js/Vitest route suite was not executed and no dependency installation was performed during this read-only audit. Validation instead used:
- exact source inspection at the pinned commit;
- source-to-sink control-flow tracing;
- inspection of existing tests and their boundary coverage;
- a deterministic local parser/control-flow harness;
- GitHub issue, PR, and commit deduplication searches.
The missing dependency environment does not weaken the central conclusion: the parse exception is visibly suppressed, the fallback state is visibly {}, and that state visibly selects the existing all-market transaction path.
Final verdict
Confirmed, High confidence, Low severity. This is a valid new correctness issue in the audited commit. It is not an authentication bypass or direct attacker primitive, but it can convert invalid authenticated input into a bounded multi-market administrative write.
Classification
POST /api/oracle/set-price-capplayground41bb1304705cd7652b49ef7a4303454d1646415cExecutive summary
The administrative price-cap route deliberately interprets an empty request body as an instruction to update every admin-oracle market controlled by the configured crank keypair. However, it implements that contract by initializing
bodyto{}and swallowing every exception raised byreq.json().As a result, an authenticated request containing non-empty but malformed or truncated JSON is indistinguishable from the intentionally empty-body command. The parse failure leaves
bodyas{}, the route selects the default cap, omits the single-market target, queries all matching markets, and can submit one on-chain transaction per validated market.This is a fail-open scope expansion in an administrative write endpoint: invalid input can expand the operation from the intended request to a batch affecting all eligible markets. This report does not claim an authentication bypass. The issue is reachable only after the existing administrative authentication and crank-key configuration checks succeed.
Affected code
app/app/api/oracle/set-price-cap/route.ts:86-105app/app/api/oracle/set-price-cap/route.ts:107-157app/app/api/oracle/set-price-cap/route.ts:158-205app/app/api/oracle/set-price-cap/route.ts:220-266Security and correctness invariant
The route violates this invariant because the parse-error state and the intentional all-market state are represented by the same value:
{}.Root cause
The vulnerable control flow is:
The
catchblock does not establish that the body is actually empty. It also catches malformed JSON, truncated JSON, unsupported encodings surfaced by the parser, and any other exception fromreq.json(). After the exception, execution continues withbody === {}.The next decisions compound the problem:
body.maxChangeE2bpsis absent, so the route assignsDEFAULT_MAX_CHANGE_E2BPS(1_000n).body.slabAddressis absent, so the route selects the all-market branch.marketstable for rows whoseoracle_authoritymatches the crank public key.SetOraclePriceCaptransaction for every retained address.The on-chain ownership checks are valuable, but they validate which accounts are eligible; they do not validate that the caller intended an all-market operation. Consequently, they do not mitigate the scope-confusion bug.
There are two additional runtime-validation symptoms. JSON
nullis valid and therefore does not enter the parser catch. The static TypeScript annotation is erased at runtime, so the next access tobody.maxChangeE2bpsthrows aTypeError. JSON arrays also bypass object-shape validation; an empty array has neither property and therefore selects the same default-cap/all-market branch as{}. This independently confirms that the route is relying on a compile-time annotation instead of validating the runtime JSON shape.Preconditions and reachability
All of the following are required for the batch-write impact:
x-admin-secretaccepted bycheckAdminSecret.CRANK_KEYPAIRis configured and parses successfully.oracle_authoritymatches the crank public key.These preconditions limit attacker reachability, but they do not eliminate operational risk. An internal client bug, a partially written request body, an automation error, or transport truncation can satisfy the malformed-body condition. Administrative endpoints should be especially strict because an input failure must not be interpreted as a broader write instruction.
Deterministic PoC
The following safe local harness uses the same WHATWG
Request.json()primitive and mirrors the route's parse/default/target decisions. It does not connect to Solana, Supabase, or the production application.Observed output on the audited snapshot's runtime environment:
{"rawBody":"","parseRejected":true,"body":{},"outcome":{"maxChangeE2bps":"1000","target":"all-admin-oracle-markets"}} {"rawBody":"{","parseRejected":true,"body":{},"outcome":{"maxChangeE2bps":"1000","target":"all-admin-oracle-markets"}} {"rawBody":"null","parseRejected":false,"body":null,"outcome":{"throws":"TypeError"}} {"rawBody":"[]","parseRejected":false,"body":[],"outcome":{"maxChangeE2bps":"1000","target":"all-admin-oracle-markets"}} {"rawBody":"{}","parseRejected":false,"body":{},"outcome":{"maxChangeE2bps":"1000","target":"all-admin-oracle-markets"}} {"rawBody":"{\"slabAddress\":\"S\"}","parseRejected":false,"body":{"slabAddress":"S"},"outcome":{"maxChangeE2bps":"1000","target":"single-market"}}The complete source-to-sink path is therefore:
Actual behavior
nullbody reaches an unhandled property access and produces a server error instead of a validation response.[]receives the default cap and enters the all-market branch rather than returning400.Expected behavior
400 Bad Requestbefore any database query, account lookup, signing, or transaction submission.null, arrays, strings, and numbers must return a stable400response.Impact
Integrity impact
An authenticated malformed request can overwrite previously configured circuit-breaker values with the route's default value across every eligible market. Markets with intentionally different caps may therefore receive the wrong configuration.
Operational impact
The transaction loop is sequential and records per-market success or failure. If some transactions land before a later transaction fails, the system can enter a partially updated state. Recovery requires determining which transactions landed and restoring the intended cap for each affected market.
Availability and cost impact
One malformed request can initiate database work, an on-chain multi-account lookup, and up to 50 signed transaction attempts. This is bounded by
MAX_SLAB_BATCH, but it is still materially more work than rejecting an invalid request at the parser boundary.Severity rationale
Severity is Low after conservative recalibration:
The issue is therefore a real fail-open administrative input bug, but its trigger is uncommon and privileged. There is no authentication bypass, crank-key compromise, arbitrary account targeting, or unprivileged attack path in this report. PR #2494 independently characterizes invalid input on this endpoint as an admin-only malformed-request problem rather than an attack; the present bug has broader scope because it can reach multiple markets, but that historical calibration supports Low rather than Medium.
Recommended fix
The safest contract is to remove “empty body means all” and require explicit scope:
{ "slabAddress": "...", "maxChangeE2bps": "1000" }or:
{ "all": true, "maxChangeE2bps": "1000" }Recommended implementation properties:
400for every parse failure.typeof value === "object",value !== null, and!Array.isArray(value).slabAddressorall: true.{},null, arrays, primitive JSON values, and unknown contradictory scope fields.If backward compatibility absolutely requires an empty body, read
await req.text()first. Only a truly empty or whitespace-only string should enter that legacy branch; a non-empty string that failsJSON.parsemust return400. Even then, explicit{ "all": true }is preferable because it makes destructive scope reviewable in logs and client code.Suggested regression tests
"{") returns400; Supabase andsendAndConfirmTransactionare not called.400and performs no side effects.nullreturns400.400.400.400when explicit scope is adopted.{ "all": true }is the only payload that enters the all-market branch.{ "slabAddress": valid }processes exactly one slab.allandslabAddressreturns400.maxChangeE2bpsreturns400before target discovery.Existing test gap
The current
oracle-set-price-cap.test.tscoverage verifies authentication failures and a non-integermaxChangeE2bps. It does not cover malformed JSON, valid non-object JSON, or an explicit distinction between single-market and all-market intent. The separate u64-bound test addresses numeric range handling, not parse-failure scope expansion.Duplicate analysis
Searches were performed against open issues, closed issues, pull requests, and commits using exact and broad variants including
set-price-cap,malformed JSON,invalid JSON,empty body, andall admin-oracle markets.Related work is materially different:
maxChangeE2bpsvalidation.maxChangeE2bpspath to u64.None of those items covers the parse-error-to-all-market control-flow transition described here. No matching issue or PR was found for malformed/invalid JSON being interpreted as an all-market operation as of 2026-08-12.
Validation limitations
The repository snapshot did not contain installed dependencies, so the complete Next.js/Vitest route suite was not executed and no dependency installation was performed during this read-only audit. Validation instead used:
The missing dependency environment does not weaken the central conclusion: the parse exception is visibly suppressed, the fallback state is visibly
{}, and that state visibly selects the existing all-market transaction path.Final verdict
Confirmed, High confidence, Low severity. This is a valid new correctness issue in the audited commit. It is not an authentication bypass or direct attacker primitive, but it can convert invalid authenticated input into a bounded multi-market administrative write.