diff --git a/services/appraisal-api/src/config.test.ts b/services/appraisal-api/src/config.test.ts index 679306b..4517f2e 100644 --- a/services/appraisal-api/src/config.test.ts +++ b/services/appraisal-api/src/config.test.ts @@ -159,3 +159,23 @@ describe("configFromEnv failure modes", () => { ); }); }); + +describe("RPC URL embedded credentials", () => { + for (const credentials of ["private-user@", ":private-password@", "private-user:private-password@", "private%2Duser:private%2Dpassword@"]) { + test(`rejects credential form ${credentials.indexOf(":") >= 0 ? "password" : "username"}`, () => { + assert.throws(() => configFromEnv({ ...MINIMAL_ENV, RPC_URL: `https://${credentials}rpc.example/` }), (error: unknown) => { + assert.ok(error instanceof AppraisalConfigError); + assert.equal(error.variable, "RPC_URL"); + assert.match(error.message, /embedded credentials/); + assert.doesNotMatch(error.stack ?? error.message, /private-user|private-password|private%2D/); + assert.equal(error.cause, undefined); + return true; + }); + }); + } + for (const RPC_URL of ["http://localhost:8000", "https://rpc.example/path"]) { + test(`accepts credential-free ${RPC_URL}`, () => { + assert.equal(configFromEnv({ ...MINIMAL_ENV, RPC_URL }).rpcUrl, RPC_URL); + }); + } +}); diff --git a/services/appraisal-api/src/config.ts b/services/appraisal-api/src/config.ts index c7e4f15..687e194 100644 --- a/services/appraisal-api/src/config.ts +++ b/services/appraisal-api/src/config.ts @@ -121,8 +121,12 @@ function parseRpcUrl( if (url.protocol !== "https:" && url.protocol !== "http:") { throw new Error("unsupported protocol"); } + if (url.username || url.password) { + throw new AppraisalConfigError("RPC_URL", "must not contain embedded credentials"); + } return url.toString().replace(/\/$/, ""); } catch (cause) { + if (cause instanceof AppraisalConfigError) throw cause; throw new AppraisalConfigError( "RPC_URL", `must be a valid http(s) URL, got ${JSON.stringify(value)}`,