arcjet / arcjet-js

Arcjet JS SDKs. Bot detection, rate limiting, email validation, attack protection, data redaction for Node.js, Next.js, Deno, Bun, Remix, SvelteKit, NestJS.
https://arcjet.com
Apache License 2.0
294 stars 8 forks source link

Allow specifying trusted proxies #2346

Open davidmytton opened 1 day ago

davidmytton commented 1 day ago

If you put your site behind a LB or proxy then they usually add headers in the form of X-Forwarded-For: <client>, <proxy1>, <proxy2>

Our current implementation is based on running on platforms like Vercel and Fly.io where we can detect the platform and trust the proxy providing us with the real client IP. That's not true on general platforms like AWS or GCP, so we can't trust the X-Forwarded-For header. Our implementation processes in reverse and picks the first public IP.

https://github.com/arcjet/arcjet-js/blob/007f51caa108792888db27ff8b0c5233e79a08cb/ip/index.ts#L743-L744

We should support the option to provide a list of one or more trusted proxies that we can ignore so our parser gets to the actual client IP.

In the example above we'd allow configuration of proxy1 and proxy2 so we'd use client as the actual IP.

See real user report of this in https://github.com/arcjet/arcjet/issues/3606

davidmytton commented 19 hours ago

Assuming you trust the proxies, this workaround will use the first IP in the X-Forwarded-For header:

export default async function middleware(request: NextRequest) {
  // Construct a request object to send to Arcjet with the required fields
  // until https://github.com/arcjet/arcjet-js/issues/2346 is resolved. This
  // is a temporary workaround to get the IP from the load balancer
  const xForwardedFor = request.headers.get("X-Forwarded-For") ?? "127.0.0.1";

  const forwardedIps = [];

  // As per MDN X-Forwarded-For Headers documentation at
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For
  // The `x-forwarded-for` header may return one or more IP addresses as
  // "client IP, proxy 1 IP, proxy 2 IP", so we want to split by the comma and
  // trim each item.
  for (const item of xForwardedFor.split(",")) {
    forwardedIps.push(item.trim());
  }

  // We trust the proxies in the `x-forwarded-for` header, so we'll use the first
  // IP address in the list.
  const ip = forwardedIps[0];

  const arcjetRequest = {
    ip,
    method: request.method,
    host: request.nextUrl.host,
    url: request.url,
    headers: request.headers,
  };

  const decision = await arcjet.protect(arcjetRequest);

  if (decision.isDenied()) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
  } else {
    return NextResponse.next();
  }
}
blaine-arcjet commented 19 hours ago

// We trust the proxies in the x-forwarded-for header, so we'll use the first // IP address in the list.

You should make an allowlist of trusted IPs because pretty much every proxy adds to any header specified. Using the first item in the list allows anyone to bypass all IP-based protections.