Skip to content

Commit 92795d2

Browse files
committed
wp-proudcity#2829: add trust-proxy-client-ip mu-plugin
Rewrites REMOTE_ADDR from the last hop of X-Forwarded-For when the request reached PHP from an RFC1918 address. Without it, every visitor shares the internal k8s pod IP and IP-based features (GF Stripe rate limiter, audit logs, geo, login throttling) misattribute traffic. Requires the nginx-ingress-lb Service to switch to externalTrafficPolicy: Local so the GCP NLB preserves the client IP up to nginx-ingress. See proudcity-kubernetes/config/default-nginx-ingress-lb-client-ip-patch.yml.
1 parent b32acb4 commit 92795d2

1 file changed

Lines changed: 60 additions & 0 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
/**
3+
* Plugin Name: Trust Proxy Client IP
4+
* Description: Rewrites $_SERVER['REMOTE_ADDR'] from the X-Forwarded-For header set by our
5+
* nginx-ingress controller, so PHP/WordPress see the real visitor IP instead of
6+
* the internal k8s pod IP. Required so anything IP-based (Gravity Forms Stripe
7+
* rate limiter, audit logs, geo, login throttling, abuse heuristics) works.
8+
* Requires nginx-ingress-lb service to use externalTrafficPolicy: Local so the
9+
* GCP NLB preserves the real client IP up to nginx-ingress. See wp-proudcity#2829.
10+
* Author: ProudCity
11+
* Version: 1.0.0
12+
*/
13+
14+
if ( ! defined( 'ABSPATH' ) ) {
15+
exit;
16+
}
17+
18+
/**
19+
* Rewrite REMOTE_ADDR from the last hop in X-Forwarded-For when the connection
20+
* comes from inside our cluster.
21+
*
22+
* Why "last hop": nginx-ingress sets the header with `$proxy_add_x_forwarded_for`,
23+
* which appends the connecting client's IP to whatever the client sent. So the
24+
* RIGHTMOST entry is the one nginx-ingress observed (trusted); leading entries
25+
* are whatever the client put there and must not be trusted. Taking the last
26+
* entry prevents spoofing via a forged X-Forwarded-For from the public client.
27+
*
28+
* Why the RFC1918 gate: WordPress pods only receive traffic from inside the
29+
* cluster (via nginx-ingress). If REMOTE_ADDR is a public address, something
30+
* unexpected is talking to us and we should not rewrite anything.
31+
*/
32+
( function () {
33+
if ( empty( $_SERVER['REMOTE_ADDR'] ) || empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
34+
return;
35+
}
36+
37+
$remote = $_SERVER['REMOTE_ADDR'];
38+
39+
// Only trust X-Forwarded-For when the request reached us from an internal
40+
// (RFC1918/reserved) address — i.e. an in-cluster proxy hop. filter_var with
41+
// these flags returns the IP only if it is NOT private/reserved (i.e. public).
42+
$is_public = filter_var(
43+
$remote,
44+
FILTER_VALIDATE_IP,
45+
FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
46+
);
47+
48+
if ( $is_public !== false ) {
49+
return;
50+
}
51+
52+
$parts = array_map( 'trim', explode( ',', $_SERVER['HTTP_X_FORWARDED_FOR'] ) );
53+
$last = end( $parts );
54+
55+
if ( ! $last || ! filter_var( $last, FILTER_VALIDATE_IP ) ) {
56+
return;
57+
}
58+
59+
$_SERVER['REMOTE_ADDR'] = $last;
60+
} )();

0 commit comments

Comments
 (0)