@@ -686,88 +686,73 @@ function _M.handle_upstream(api_ctx, route, enable_websocket)
686686end
687687
688688
689- local function handle_x_forwarded_headers (api_ctx )
690- local addr_is_trusted = trusted_addresses_util .is_trusted (api_ctx .var .realip_remote_addr )
691-
692- -- Only untrusted values need to be overwritten or cleared.
693- if not addr_is_trusted then
694- -- store the original x-forwarded-* headers
695- -- to allow future use by other plugins or processes
696- api_ctx .var .original_x_forwarded_proto = api_ctx .var .http_x_forwarded_proto
697- api_ctx .var .original_x_forwarded_host = api_ctx .var .http_x_forwarded_host
698- api_ctx .var .original_x_forwarded_port = api_ctx .var .http_x_forwarded_port
699- api_ctx .var .original_x_forwarded_for = api_ctx .var .http_x_forwarded_for
700-
701- -- trusted ones
702- -- ref: ngx_tpl.lua#L831-L840
703- --
704- -- these values are observed directly by APISIX and cannot be forged,
705- -- making them highly credible.
706- local proto = api_ctx .var .scheme
707- local http_host = api_ctx .var .http_host or api_ctx .var .host
708- -- parse_addr handles IPv6 literals and bracketed host:port correctly.
709- local _ , port_from_host = core .utils .parse_addr (http_host )
710- local host = http_host
711- local port = port_from_host or api_ctx .var .server_port
712-
713- -- override the x-forwarded-* headers to the trusted ones.
714- -- make sure that the correct values are obtained
715- -- in the subsequent stages using `core.request.header`.
716- core .request .set_header (api_ctx , " X-Forwarded-Proto" , proto )
717- core .request .set_header (api_ctx , " X-Forwarded-Host" , host )
718- core .request .set_header (api_ctx , " X-Forwarded-Port" , port )
719- -- Clear RFC 7239 Forwarded header to prevent forgery.
720- core .request .set_header (api_ctx , " Forwarded" , nil )
721-
722- -- X-Forwarded-For: when a trust boundary is configured but this peer is
723- -- untrusted, reset it so the upstream only sees the APISIX-observed
724- -- connection IP via `$proxy_add_x_forwarded_for`, dropping the spoofable
725- -- inbound chain. When `trusted_addresses` is unset, keep the compatible
726- -- default of preserving the inbound chain (the connection IP is appended).
727- if trusted_addresses_util .is_configured () then
728- core .request .set_header (api_ctx , " X-Forwarded-For" , nil )
729- api_ctx .var .http_x_forwarded_for = nil
730- end
731-
732- -- update the cached value in http_x_forwarded_* to the trusted ones.
733- -- make sure that the correct values are obtained
734- -- in the subsequent stages using `var.http_x_forwarded_*`.
735- api_ctx .var .http_x_forwarded_proto = proto
736- api_ctx .var .http_x_forwarded_host = host
737- api_ctx .var .http_x_forwarded_port = port
738- api_ctx .var .http_forwarded = nil
689+ -- X-Forwarded-Proto/Host/Port and Forwarded are already neutralized by the time
690+ -- this runs: `more_set_input_headers` in apisix/cli/ngx_tpl.lua does it in the
691+ -- rewrite phase, in C, on every request. That is unconditional because with no
692+ -- trust boundary configured -- the default -- it is what every request needs, and
693+ -- keeping it in the config keeps Lua off that path entirely.
694+ --
695+ -- What is left needs a trust decision, so it stays here, behind a check that is a
696+ -- constant for the worker's lifetime: with no `trusted_addresses` this returns on
697+ -- its first line and nothing else runs.
698+ --
699+ -- `set` captures an absent header as the empty string, so "" means the peer sent
700+ -- nothing and the value the config injected stays. That is a deliberate change
701+ -- for a trusted peer: the Lua-only implementation skipped the whole rewrite for
702+ -- one, so a header it did not send stayed absent and the upstream fell through to
703+ -- `$host` / `$server_port`. A trusted peer now gets the same observed values an
704+ -- untrusted one does -- the Host with its port and case, rather than the
705+ -- lower-cased portless `$host` -- which is the value the untrusted path has always
706+ -- produced. `ctx.var.http_x_forwarded_*` is updated alongside, so a plugin reading
707+ -- it in a later phase sees the restored value rather than the injected one.
708+ local function restore_if_sent (api_ctx , header_name , var_name , orig )
709+ if not orig or orig == " " then
710+ return
739711 end
712+
713+ core .request .set_header (api_ctx , header_name , orig )
714+ api_ctx .var [var_name ] = orig
740715end
741716
742717
743- -- in ngx_tpl.lua#L831-L840,
744- -- there is such code: `proxy_set_header X-Forwarded-XXX $var_x_forwarded_xxx;`
745- -- that is, set the `X-Forwarded-XXX` header through `var_x_forwarded_xxx`.
746- --
747- -- therefore, it is necessary to set the trusted `http_x_forwarded_xxx` to `var_x_forwarded_xxx`.
748- -- So that the `X-Forwarded-XXX` header is updated to a trusted value.
749- --
750- -- currently, only following headers are updated through these variables:
751- -- - X-Forwarded-Proto
752- -- - X-Forwarded-Port
753- -- - X-Forwarded-Host
754- --
755- -- the `X-Forwarded-For` header is not updated through these variables.
756- -- because it is set by the `proxy_add_x_forwarded_for` directive.
757- local function set_upstream_x_forwarded_headers (api_ctx )
758- local proto = api_ctx .var .http_x_forwarded_proto
759- if proto then
760- api_ctx .var .var_x_forwarded_proto = proto
718+ local function handle_trusted_x_forwarded_headers (api_ctx )
719+ -- The other four originals are copied by the configuration; this one cannot be,
720+ -- because naming `$http_x_forwarded_for` there would pin it in `r->variables[]`
721+ -- and the clear below could not dislodge it. Copy it here instead, on every
722+ -- path: the header is only destroyed further down, but a plugin reading
723+ -- `ctx.var.original_x_forwarded_for` should not have to know that.
724+ local inbound_xff = api_ctx .var .http_x_forwarded_for
725+ if inbound_xff then
726+ api_ctx .var .original_x_forwarded_for = inbound_xff
761727 end
762728
763- local port = api_ctx .var .http_x_forwarded_port
764- if port then
765- api_ctx .var .var_x_forwarded_port = port
729+ if not trusted_addresses_util .is_configured () then
730+ return
766731 end
767732
768- local host = api_ctx .var .http_x_forwarded_host
769- if host then
770- api_ctx .var .var_x_forwarded_host = host
733+ if trusted_addresses_util .is_trusted (api_ctx .var .realip_remote_addr ) then
734+ -- a trusted peer's own values go back, from the copies the config took
735+ -- before overwriting them
736+ restore_if_sent (api_ctx , " X-Forwarded-Proto" , " http_x_forwarded_proto" ,
737+ api_ctx .var .original_x_forwarded_proto )
738+ restore_if_sent (api_ctx , " X-Forwarded-Host" , " http_x_forwarded_host" ,
739+ api_ctx .var .original_x_forwarded_host )
740+ restore_if_sent (api_ctx , " X-Forwarded-Port" , " http_x_forwarded_port" ,
741+ api_ctx .var .original_x_forwarded_port )
742+ restore_if_sent (api_ctx , " Forwarded" , " http_forwarded" ,
743+ api_ctx .var .original_forwarded )
744+
745+ return
746+ end
747+
748+ -- An untrusted peer, with a trust boundary to measure it against: drop the
749+ -- inbound X-Forwarded-For so the upstream only sees the connection IP via
750+ -- `$proxy_add_x_forwarded_for`. Without a boundary the chain is preserved,
751+ -- which is the compatible default and is why this lives behind the check
752+ -- above rather than in the config.
753+ if inbound_xff then
754+ core .request .set_header (api_ctx , " X-Forwarded-For" , nil )
755+ api_ctx .var .http_x_forwarded_for = nil
771756 end
772757end
773758
@@ -828,7 +813,7 @@ function _M.http_access_phase()
828813 -- var.request is read-only; copy to a writable variable so data-mask can redact query params
829814 api_ctx .var .request_line = api_ctx .var .request
830815
831- handle_x_forwarded_headers (api_ctx )
816+ handle_trusted_x_forwarded_headers (api_ctx )
832817
833818 -- When match_uri_encoded_slash is on, match the route against a uri that
834819 -- keeps the encoded slash (%2F) so it is treated as part of a path
@@ -970,10 +955,6 @@ function _M.http_access_phase()
970955 end
971956 span :finish (ngx_ctx )
972957
973- -- set before handle_upstream: grpc/dubbo/disable_proxy_buffering exit via
974- -- ngx.exec() and never return, so the trusted values must be applied first.
975- set_upstream_x_forwarded_headers (api_ctx )
976-
977958 _M .handle_upstream (api_ctx , route , enable_websocket )
978959end
979960
0 commit comments