Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | 28x 28x 47x 47x 185x 47x | import { RequestCorrelation } from '@/core/observability/RequestCorrelation'
import type { RequestFacts, RequestMetadata } from '@bayudwiyansatria/core'
/**
* Reads Cloudflare's account of a request.
*
* @remarks
* The single place in this codebase that knows what `cf-connecting-ip`,
* `cf-ray`, and `request.cf` are. Before 1.2.0 those names were read inline by
* the security middleware and the request logger, which is what kept two
* otherwise platform-agnostic pieces of HTTP plumbing tied to one vendor.
*
* Three of the choices here are security decisions rather than parsing details:
*
* - **`CF-Connecting-IP`, never `X-Forwarded-For`.** Cloudflare sets the former
* at the edge and overwrites it on every request; the latter is whatever the
* caller sent, because Cloudflare *appends* to it rather than replacing it.
* Anything trusting `X-Forwarded-For` can be told any address it likes —
* including by whatever rate limiting or audit trail reads the value back.
* - **`CF-Connecting-IPv6` ahead of it.** A zone with Pseudo-IPv4 set to
* overwrite headers replaces `CF-Connecting-IP` with a synthetic address in
* `240.0.0.0/4` for every IPv6 client. It looks like an ordinary address, so
* nothing downstream notices — but it is reserved space that routes nowhere,
* geolocates to nothing, and need not be stable between requests from one
* client. That is worse than logging no address at all, because it reads as a
* real answer: an abuse investigation follows it and finds nobody. Reading
* the IPv6 header first means the true address wins wherever the zone offers
* it, whatever the Pseudo-IPv4 setting happens to be.
* - **`CF-Ray` as the request id.** It is the id Cloudflare's own logs use, so a
* line here and a line in the dashboard can be joined. When it is absent — a
* local `wrangler dev` run, a test — a UUID stands in, so correlation degrades
* to per-process rather than disappearing.
*
* ## Why an inbound header wins over `CF-Ray`
*
* A Service Binding hop is a subrequest the edge never sees, so the callee gets
* no `CF-Ray` of its own and would otherwise mint a UUID — one request, three
* Workers, three unrelated ids, and no way to walk a failure back to its cause.
* {@link RequestCorrelation.HEADER} is what the caller sends to prevent that,
* and it is read first precisely so the *upstream* id is the one the whole
* fan-out shares.
*
* The cost is that the value is attacker-controlled at the edge: a public
* caller may send any `X-Request-Id` it likes. That is tolerable because the id
* does nothing but join log lines — the worst a forged one achieves is two
* unrelated requests appearing under one id in a query. It is not a credential,
* nothing authorises against it, and nothing here may start.
*
* @class
*
* @author Bayu Dwiyan Satria
* @version 1.2.1
* @since 1.0.0
*/
export class CloudflareRequestMetadata implements RequestMetadata<Request> {
/**
* Reads what Cloudflare knows about a request.
*
* @param request The incoming request.
* @returns The facts, with `colo` and `country` absent outside the edge.
*/
public read(request: Request): RequestFacts {
const cf = request ? (request.cf as IncomingRequestCfProperties | undefined) : undefined
const header = (name: string): string | undefined =>
request && request.headers ? request.headers.get(name) || undefined : undefined
return {
requestId: header(RequestCorrelation.HEADER) || header('cf-ray') || crypto.randomUUID(),
clientIp: header('cf-connecting-ipv6') || header('cf-connecting-ip') || 'unknown',
colo: cf ? cf.colo : undefined,
country: cf ? (cf.country as string) : undefined
}
}
}
|