mirror of
https://github.com/teamchong/pxpipe.git
synced 2026-07-22 02:02:51 +02:00
fix(worker): require PXPIPE_WORKER_SECRET when an API-key override is set
If ANTHROPIC_API_KEY / OPENAI_API_KEY is configured on a deployed Worker, the URL becomes an open key-spender: anyone who discovers it gets requests signed with the deployment's key. Now: - keys set + no PXPIPE_WORKER_SECRET -> 503, refuses to proxy at all - keys set + secret -> callers must send x-pxpipe-secret (compared via SHA-256 digests to avoid a prefix-timing signal); 401 otherwise - no keys set -> unchanged (callers bring their own credentials) The secret is stripped before forwarding upstream.
This commit is contained in:
@@ -39,6 +39,27 @@ export interface Env {
|
||||
* Cloudflare ingests console.log as Workers Logs; pipe via Logpush to
|
||||
* R2/S3 for the same JSONL shape Node writes to disk. */
|
||||
PXPIPE_TRACK?: string;
|
||||
/** Shared secret callers must present via the `x-pxpipe-secret` header
|
||||
* whenever an API-key override is configured. Without this gate a
|
||||
* discovered workers.dev URL is an open key-spender: the Worker would
|
||||
* attach your key to any stranger's request. Set with:
|
||||
* npx wrangler secret put PXPIPE_WORKER_SECRET */
|
||||
PXPIPE_WORKER_SECRET?: string;
|
||||
}
|
||||
|
||||
/** Compare SHA-256 digests instead of the raw strings so the comparison
|
||||
* can't leak a prefix-match timing signal. */
|
||||
async function secretsMatch(a: string, b: string): Promise<boolean> {
|
||||
const enc = new TextEncoder();
|
||||
const [da, db] = await Promise.all([
|
||||
crypto.subtle.digest('SHA-256', enc.encode(a)),
|
||||
crypto.subtle.digest('SHA-256', enc.encode(b)),
|
||||
]);
|
||||
const va = new Uint8Array(da);
|
||||
const vb = new Uint8Array(db);
|
||||
let diff = 0;
|
||||
for (let i = 0; i < va.length; i++) diff |= (va[i] ?? 0) ^ (vb[i] ?? 0);
|
||||
return diff === 0;
|
||||
}
|
||||
|
||||
const truthy = (v: string | undefined, fallback: boolean): boolean =>
|
||||
@@ -46,6 +67,34 @@ const truthy = (v: string | undefined, fallback: boolean): boolean =>
|
||||
|
||||
export default {
|
||||
async fetch(req: Request, env: Env, _ctx: ExecutionContext): Promise<Response> {
|
||||
// ── Caller auth ────────────────────────────────────────────────────
|
||||
// If this deployment injects API keys, never serve anonymous callers:
|
||||
// workers.dev URLs are discoverable, and without this gate anyone who
|
||||
// finds the URL spends this deployment's API credits.
|
||||
if (env.ANTHROPIC_API_KEY || env.OPENAI_API_KEY) {
|
||||
if (!env.PXPIPE_WORKER_SECRET) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error:
|
||||
'refusing to proxy: an API key override is configured but PXPIPE_WORKER_SECRET is not, ' +
|
||||
'which would let anyone who finds this URL spend the configured key. ' +
|
||||
'Run `npx wrangler secret put PXPIPE_WORKER_SECRET` and send the value as the x-pxpipe-secret header.',
|
||||
}),
|
||||
{ status: 503, headers: { 'content-type': 'application/json' } },
|
||||
);
|
||||
}
|
||||
const presented = req.headers.get('x-pxpipe-secret') ?? '';
|
||||
if (!(await secretsMatch(presented, env.PXPIPE_WORKER_SECRET))) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'missing or invalid x-pxpipe-secret header' }),
|
||||
{ status: 401, headers: { 'content-type': 'application/json' } },
|
||||
);
|
||||
}
|
||||
// Don't forward the shared secret upstream.
|
||||
req = new Request(req);
|
||||
req.headers.delete('x-pxpipe-secret');
|
||||
}
|
||||
|
||||
const transform: TransformOptions = {
|
||||
compress: truthy(env.COMPRESS, true),
|
||||
compressTools: truthy(env.COMPRESS_TOOLS, true),
|
||||
|
||||
Reference in New Issue
Block a user