Skip to content

Obtaining caller ip address with Lambda FURLs

Steampunk villain breaking into a bank vault

If you use Lambda Function URLs and are extracting the caller's ip address from the x-forwarded-for header, that ip can be spoofed. In this article, I'll describe the scenarios where Lambda mangles this header and a workaround to get the caller's true ip address. Since several frameworks rely on the x-forwarded-for to determine the caller's ip address, your caller's ip address may be wrong!

What is the x-forwarded-for header?

The x-forwarded-for header is the standard HTTP header used to relay the client's original ip address when a request is proxied through a load balancer or reverse proxy. This header can be set to any value by the client, but trusted proxies including CloudFront or API Gateway append the true client's ip address to the rightmost position of this header per the specification.

  1. If x-forwarded-for is not present, the header is set to the client's ip address and becomes x-forwarded-for: <caller-ip>
  2. If x-forwarded-for is present i.e. x-forwarded-for: 127.0.0.1 after passing through a trusted proxy, the client's ip address is appended to the rightmost position and becomes: x-forwarded-for: 127.0.0.1, <caller-ip>

How I discovered this

During my daily dashboard review, I noticed requests for non-existent api paths coming from the ip 127.0.0.1. Curiously, CloudFront was reporting the caller was in France. 127.0.0.1 is a reserved ip address for your local machine, so it was being spoofed, but how? I was using the rightmost value of the x-forwarded-for header. Turns out, when I logged the x-forwarded-for header in my Lambda, it was just 127.0.0.1. Lambda Function Urls truncate the x-forwarded-for header to the leftmost value. Lambda doesn't modify other headers however.

What is the x-forwarded-for header value in common scenarios?

To illustrate how the x-forwarded-for header can be mangled by Lambda, I've created a table with common scenarios. For each test, I show the x-forwarded-for header sent by the client, the value of x-forwarded-for header received at Lambda, and the value of the ip available in the Lambda event object. I tested a Lambda Function Url exposed to the internet, a Lambda function url fronted by CloudFront and a Lambda fronted by API Gateway.

Test client x-forwarded-for lambda x-forwarded-for event.requestContext.http.sourceIp or
event.requestContext.identity.sourceIp
Lambda FURL 127.0.0.1 127.0.0.1 <caller ip>
Lambda FURL <caller-ip> <caller ip>
Lambda FURL 127.0.0.1, 24.56.128.35 127.0.0.1 <caller ip>
Lambda FURL 127.0.0.1; 24.56.128.35 127.0.0.1; 24.56.128.35 <caller ip>
CloudFront FURL 127.0.0.1 127.0.0.1 <cloudfront edge ip>
CloudFront FURL <caller-ip> <cloudfront edge ip>
CloudFront FURL 127.0.0.1, 24.56.128.35 127.0.0.1 <cloudfront edge ip>
CloudFront FURL 127.0.0.1; 24.56.128.35 127.0.0.1; 24.56.128.35 <cloudfront edge ip>
REST APIGateway 127.0.0.1 127.0.0.1, <caller ip>, <apig edge ip> <caller ip>
REST APIGateway <caller ip>, <apig edge ip> <caller ip>
REST APIGateway 127.0.0.1, 24.56.128.35 127.0.0.1, 24.56.128.35, <caller-ip>, <apig edge ip> <caller ip>
REST APIGateway 127.0.0.1; 24.56.128.35 127.0.0.1; 24.56.128.35, <caller-ip>, <apig edge ip> <caller ip>

To summarize, when using Lambda function URLs, the x-forwarded-for header is truncated to the leftmost value or set to the caller's ip address when not present. The value of event.requestContext.http.sourceIp is only set to the correct value if you are connecting directly to the Lambda function url without using a proxy like CloudFront. The value of event.requestContext.identity.sourceIp is always set correctly when using API Gateway.

How do I get the caller's IP when using CloudFront

With CloudFront you can attach a CloudFront function to the viewer request to add a custom header with the caller's ip address.

async function handler(event) {
    event.request.headers["x-ip"] = { value: event.viewer.ip };
    return event.request;
}

This is accessible in your Lambda as event.headers['x-ip'].

Tip

Several frameworks use the x-forwarded-for header. If you are using hono, lambda-api, fastify, or express.js, the caller's ip address will be taken from x-forwarded-for. Since Lambda reports the leftmost value instead of the actual value, to reliably get the caller's ip address, inject it into a custom header using a CloudFront function and rely on that instead.

What should Lambda be doing for function URLs?

I'd argue they should just append the caller's ip address to the x-forwarded-for header like how it is done for the API Gateway integration. However, it may be too late for them to change this behavior so at least it should be documented.

Conclusion

If you depend on your caller's ip address for auditing or throttling purposes and use CloudFront with Lambda function URLs, don't use x-forwarded-for to determine the caller's ip address. Instead use a CloudFront function to add the caller's ip address to the request headers and use that.

Further Reading

  1. The X-Forwarded-For specification.
  2. Writing Cloudfront Functions
  3. The Lambda Function URL request payload format