Reviewing for leaks
Lens one, on real screener code. Here is a proxy route an assistant produced for "let the page pass screener filters through".
export async function GET(req: NextRequest) {
const incoming = new URL(req.url).searchParams;
const upstream = new URL("https://eodhd.com/api/screener");
for (const [k, v] of incoming.entries()) {
upstream.searchParams.set(k, v);
}
upstream.searchParams.set("api_token", process.env.EODHD_API_KEY!);
const res = await fetch(upstream);
return Response.json(await res.json());
}
It works. It is also three separate leaks. Find them before reading on.
Leak one: the caller sets the token
The loop copies every parameter, including api_token. A caller who sends one has theirs overwritten by the server's on the next line, so that specific ordering saves you here — but reorder those two lines during a refactor and your proxy starts making requests with a stranger's token, or worse, with a token they control.
Course 1's proxy dropped api_token explicitly. This one relies on line order. Explicit beats lucky:
if (k.toLowerCase() === "api_token") continue;
Leak two: the caller sets the limit
Nothing clamps limit. limit=500 was verified to return 500 rows in one call, and a loop of those requests spends your daily allowance in minutes. Your quota is a resource a stranger can consume through a public URL, which makes this a denial-of-service against yourself.
Leak three, and this is the one people miss
The response is forwarded verbatim: Response.json(await res.json()).
Today that is screener rows and harmless. But the same pattern applied to /user forwards the account's email, payment method and invite token — which is precisely the mistake course 1 warned about, arriving here in a different costume. A proxy that reflects upstream responses unexamined is a proxy that will one day reflect something private.
Return what the page needs, not what the upstream said.
And a fourth thing that is not a leak but will bite
process.env.EODHD_API_KEY! — the non-null assertion. With the variable unset, the request goes out with api_token=undefined, the API rejects it, and your page shows an empty screen. The person debugging it has no idea the key is missing, because nothing said so. Course 1 threw a named error for exactly this.
What the review comment looks like
Three things. The parameter loop copies
api_token, so this only works because the server'ssethappens to run afterwards — drop it explicitly instead of relying on line order.limitis unclamped and a stranger can spend my quota through the public URL, so clamp it here rather than in the page. And the response is forwarded verbatim; return only the fields the table needs, because this pattern applied to another endpoint forwards an email address. Separately, the!on the env var turns a missing key into a blank page rather than a message.
Four sentences, each naming a consequence. That is the standard from lesson 1 of this unit, applied.
Try it now
Open your own screener proxy and check it against these four. Then try them from outside: curl your deployed URL with api_token=fake, with limit=99999, and with the key removed from the environment. Three attacks, three specific behaviours you should be able to predict before you run them.