From 4684ec416bebe93b05eefb4d5d5e383f1d49148f Mon Sep 17 00:00:00 2001 From: s6pa1rta3n-lab Date: Tue, 1 Sep 2026 19:14:05 -0400 Subject: [PATCH] fix(appraisal): reject credentials embedded in RPC URLs --- services/appraisal-api/src/config.test.ts | 48 +++++++++++++++++++++++ services/appraisal-api/src/config.ts | 13 +++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/services/appraisal-api/src/config.test.ts b/services/appraisal-api/src/config.test.ts index 679306b..c35ea09 100644 --- a/services/appraisal-api/src/config.test.ts +++ b/services/appraisal-api/src/config.test.ts @@ -81,6 +81,15 @@ describe("configFromEnv valid configurations", () => { assert.equal(config.networkPassphrase, Networks.PUBLIC); assert.equal(config.asset, USDC_PUBNET_ADDRESS); }); + + test("accepts valid HTTP RPC URL control case", () => { + const config = configFromEnv({ + ...MINIMAL_ENV, + RPC_URL: "http://localhost:8000/rpc", + }); + + assert.equal(config.rpcUrl, "http://localhost:8000/rpc"); + }); }); describe("configFromEnv failure modes", () => { @@ -158,4 +167,43 @@ describe("configFromEnv failure modes", () => { "RPC_URL", ); }); + + test("rejects RPC URLs containing credentials without echoing them", () => { + const cases = [ + { + url: "https://alice:secret123@rpc.example.com", + credentials: ["alice", "secret123"], + }, + { + url: "https://alice@rpc.example.com", + credentials: ["alice"], + }, + { + url: "https://:secret123@rpc.example.com", + credentials: ["secret123"], + }, + { + url: "http://admin:pass@localhost:8000", + credentials: ["admin", "pass"], + }, + ]; + + for (const { url, credentials } of cases) { + assert.throws( + () => configFromEnv({ ...MINIMAL_ENV, RPC_URL: url }), + (error: unknown) => { + assert.ok(error instanceof AppraisalConfigError); + assert.equal(error.variable, "RPC_URL"); + assert.match(error.message, /must not contain credentials/); + for (const secret of credentials) { + assert.ok( + !error.message.includes(secret), + `Error message should not echo credential: ${secret}`, + ); + } + return true; + }, + ); + } + }); }); diff --git a/services/appraisal-api/src/config.ts b/services/appraisal-api/src/config.ts index c7e4f15..693d642 100644 --- a/services/appraisal-api/src/config.ts +++ b/services/appraisal-api/src/config.ts @@ -116,12 +116,12 @@ function parseRpcUrl( return DEFAULT_TESTNET_RPC_URL; } + let url: URL; try { - const url = new URL(value); + url = new URL(value); if (url.protocol !== "https:" && url.protocol !== "http:") { throw new Error("unsupported protocol"); } - return url.toString().replace(/\/$/, ""); } catch (cause) { throw new AppraisalConfigError( "RPC_URL", @@ -129,6 +129,15 @@ function parseRpcUrl( { cause }, ); } + + if (url.username || url.password) { + throw new AppraisalConfigError( + "RPC_URL", + "must not contain credentials", + ); + } + + return url.toString().replace(/\/$/, ""); } function validateFacilitatorSecret(secret: string): void {