skip to content

A federated subgraph trusts an x-user-id header its router sets. Why is that unsafe?

level: seniorimportance: should knowfreq 43%

answer

  1. A header is a claim, not proof
  2. Who else can reach that port?
  3. Prefix rules forward the caller's forgery
  4. Strip inbound, allowlist outbound
  5. Verify a signature instead of trusting

basics

~20 s

The header is unverified text, so the subgraph is trusting whatever can open a connection to it. It is safe only if nothing but the router can reach the subgraph and the router strips any copy the caller sent.

solid answer

~50 s

A plain identity header carries no proof. Reading one and treating its value as the authenticated caller delegates authentication to the network, which holds only while two deployment properties hold: the subgraph is unreachable except through the router, and the router cannot be tricked into passing the caller's own copy of that header through. Both break easily. A prefix rule that forwards every header starting with `x-` lets a client set `x-user-id` itself; a subgraph is an ordinary HTTP service that any neighbour, batch job or port-forward can reach. The stronger design has the subgraph verify something: the caller's signed token forwarded intact, or a short-lived assertion the router mints naming that subgraph as audience. Do both — verify the credential and isolate the service — because spoofed identity leaves no trace in any log you would think to check.

code

pseudocode · 15 lines
pseudocode
// Unsafe router rule: whatever the caller sent with an x- prefix
// is copied onto every subgraph fetch.
for name in inbound.headerNames():
    if name.startsWith("x-"):
        outbound.setHeader(name, inbound.getHeader(name))

// Client sends:  x-user-id: sup_9002
// Subgraph reads: sup_9002, and believes it.

// Safer: deny by default, and strip anything the router itself sets.
outbound.removeHeader("x-user-id")
for name in ALLOWLIST:                       // e.g. ["authorization"]
    if inbound.hasHeader(name):
        outbound.setHeader(name, inbound.getHeader(name))
outbound.setHeader("x-user-id", verifiedCaller.id)

go deeper

for a junior

Recall that headers are just text a caller can set. If a service believes a header without checking anything, then anyone able to send it a request can claim to be anyone.

for a middle

Explain the two conditions that make a trusted identity header workable — the service is reachable only from the router, and the router strips caller-supplied copies — and how a wildcard forwarding rule breaks the second.

for a senior

Show the production judgement: replace trust in the network with a verifiable credential, keep network isolation as a second layer, and explain why this failure produces clean-looking logs and therefore no alert.

for a principal

Own the trust architecture across services: credential audience and lifetime, key rotation, whether the router becomes a signing service, and how a single router misconfiguration is scoped so it cannot become a graph-wide bypass.

## A header is a claim, not proof `x-user-id: sup_4471` is text. It says a caller exists; it proves nothing about who sent it. When a subgraph reads that header and treats its value as the authenticated caller, it has not authenticated anyone — it has delegated authentication to *whatever managed to open a TCP connection to it*. That is a fine decision if, and only if, two conditions hold, and both are properties of the deployment rather than of the code: 1. **Nothing but the router can reach the subgraph.** 2. **The router cannot be tricked into passing a caller's own copy of that header through.** Break either one and every authorization rule in the subgraph evaluates against an attacker-chosen identity. In a charity donations graph, that is every supporter's giving history, and it is silent. ## How the second condition breaks: prefix passthrough Header propagation is configured in the router, and the tempting shortcut is a pattern rule: forward everything beginning with `x-`. It is one line, it never needs touching when a new header appears, and it hands the caller the keys. A client sets `x-user-id: sup_9002` on its own request to the router; the rule copies it onto every subgraph fetch; the subgraphs believe it. Nothing in the router's logs looks wrong, because from the router's point of view it did exactly what it was told. The correct policy is deny-by-default and boring: an explicit allowlist of header names per subgraph, and an explicit **strip** of any inbound header the router itself intends to set. The strip matters as much as the allowlist — if the router sets `x-caller-id` after having already merged inbound headers, order of operations decides whether you have a vulnerability. ## How the first condition breaks: subgraphs are ordinary HTTP servers A subgraph is a normal service listening on a normal port. Anything on the same network can talk to it: another subgraph, a batch job, a developer's port-forward, an ingress rule that was meant to be internal, a compromised neighbour with no relationship to the graph at all. "Internal network" is a reduction in exposure, not a proof of origin. And an attacker who can reach the subgraph directly does not merely spoof identity — they choose the entry point too, including `_entities` with representations of their own making. ## The alternative: make the subgraph verify rather than trust Forward a credential the subgraph can check by itself. Two common shapes: * **Pass the caller's signed token through.** The subgraph verifies signature, expiry and intended audience against the issuer's public keys. A forged header is now a forged signature, which is not a thing an attacker can produce. * **Have the router mint a short-lived signed assertion per subgraph.** The router verifies the caller once, then issues a narrowly-scoped, briefly-valid token naming that subgraph as its audience. The subgraph verifies cryptographically; a token stolen from one subgraph is useless at the next. Both replace *trust in the network* with *trust in a key*, which is auditable, rotatable, and does not quietly evaporate when someone adds an ingress rule. ## Do both anyway This is a defence-in-depth argument, not an either/or. Verification stops forged identity. Network isolation and mutual TLS stop an attacker reaching the subgraph in the first place, and stop the entity-path and introspection problems that identity verification alone does nothing about. A graph that verifies but is publicly reachable, or one that is isolated but trusts headers, has exactly one thing between an attacker and the donations table. ## Why this failure is worse than it looks Identity spoofing is invisible in exactly the places you would look. The subgraph's audit log records `sup_9002` because that is what it was told; the router's log records a request it forwarded faithfully; the metrics show ordinary traffic. There is no signature failure, no 401, no anomaly. You do not detect this class of bug from telemetry — you detect it by asking, during design review, "what stops something other than the router from sending this?" and requiring an answer that is not "it is on the internal network". ## Grading the answer A strong candidate says the header is unverified input, names both preconditions, points at the prefix-passthrough rule as the concrete way the first one gets broken, and then says what they would change: allowlist and strip in the router, verify a signed credential in the subgraph, isolate the subgraph at the network layer, and treat the router itself as security-critical infrastructure — because in this design a router misconfiguration is a full authorization bypass across every service behind it.

  • The subgraphs sit on a private network. Does that make the header safe?
    It reduces exposure without proving origin. Any neighbouring service, batch job, developer port-forward or over-broad ingress rule can still reach an ordinary HTTP port, and none of them are the router. Treat network isolation as one layer that limits who can try, and cryptographic verification as the layer that decides who is believed. A design that has only one of the two is one configuration change from a full bypass.
  • What does a sound header policy in the router look like?
    Deny by default. An explicit allowlist of header names, ideally per subgraph, and an explicit strip of every header the router intends to set itself so a client's copy cannot survive. Order matters: strip before you set, or an inbound value can win. Never use a prefix or wildcard rule, and review the policy the way you would review an authorization rule, because that is what it is.
  • Why is spoofed identity of this kind hard to detect after the fact?
    Because nothing fails. The subgraph logs the identity it was handed, the router logs a request it forwarded faithfully, and there is no signature error, no 401 and no traffic anomaly to alert on. The audit trail records the attacker's chosen user id as though it were fact. You catch this in design review by asking what prevents something other than the router from sending the header, not in telemetry.

It is the difference between a visitor badge the front desk prints and checks against a register, and one where the sticker itself is the only evidence — fine until anyone else can find a sticker printer.

saying these in an interview costs you the question

  • Treats a router-set user-id header as proof of identity
  • Forwards every caller header matching an x- prefix
  • Assumes only the router can reach a subgraph's port
  • Thinks a private network makes forged headers impossible
  • Expects header spoofing to show up in subgraph logs

context