Skip to content

fix: only trust X-Forwarded-* headers from local/LAN proxies#1987

Merged
TrystanLea merged 1 commit into
masterfrom
fix/trusted-proxy-host-header-injection
May 11, 2026
Merged

fix: only trust X-Forwarded-* headers from local/LAN proxies#1987
TrystanLea merged 1 commit into
masterfrom
fix/trusted-proxy-host-header-injection

Conversation

@TrystanLea
Copy link
Copy Markdown
Member

@TrystanLea TrystanLea commented May 9, 2026

Important for anyone self-hosting emoncms in public web accessible context.

LLM (Claude) security audit suggestion:

Forwarded headers (X-Forwarded-Host, X-Forwarded-Proto, X-Forwarded-Port) can be injected by remote attackers to poison the application base URL, enabling password reset token hijacking, open redirects, cache poisoning, and JavaScript asset replacement.

Introduce is_trusted_proxy() which validates REMOTE_ADDR against loopback and RFC 1918 private ranges before forwarded headers are acted upon. Public IPs are untrusted and fall back to the real Host header.

Local tunnel services (Dataplicity, ngrok) and reverse proxies (nginx, HA ingress) continue to work automatically as their agents connect via 127.0.0.1 or a LAN address.

Immediate solution: Use manual domain setting
There is a manual domain setting that can be set in settings.ini that avoids the use of X-Forwarded-... The manual setting has been in place for a long time and is the approach used by emoncms.org.

Forwarded headers (X-Forwarded-Host, X-Forwarded-Proto, X-Forwarded-Port)
can be injected by remote attackers to poison the application base URL,
enabling password reset token hijacking, open redirects, cache poisoning,
and JavaScript asset replacement.

Introduce is_trusted_proxy() which validates REMOTE_ADDR against loopback
and RFC 1918 private ranges before forwarded headers are acted upon. Public
IPs are untrusted and fall back to the real Host header.

Local tunnel services (Dataplicity, ngrok) and reverse proxies (nginx, HA
ingress) continue to work automatically as their agents connect via
127.0.0.1 or a LAN address.
@TrystanLea
Copy link
Copy Markdown
Member Author

@alexandrecuer interested in your feedback on this one if you have a moment.

@bwduncan
Copy link
Copy Markdown
Contributor

bwduncan commented May 9, 2026

Good idea, but what does Claude think this is going to do for IPv6?

Edit: Oh I see it's accepting ::1. That's fine.

@alexandrecuer
Copy link
Copy Markdown
Contributor

alexandrecuer commented May 10, 2026

@TrystanLea : I've seen that fix/trusted-proxy-host-header-injection is on version 11.11.3

Last time I've built for 11.9.11

module last version used for container build actual version
https://github.com/emoncms/graph 2.2.7 2.2.7
https://github.com/emoncms/dashboard 2.4.3 2.4.4
https://github.com/emoncms/app 3.1.5 3.1.7
https://github.com/emoncms/device 2.3.3 2.3.10
symodule last version used for container build actual version
https://github.com/emoncms/sync 3.2.2 3.2.5
https://github.com/emoncms/postprocess 2.4.9 2.5.3
https://github.com/emoncms/backup 2.3.5 2.3.5

I have built with that fix, using last versions of modules and symodules : https://hub.docker.com/layers/alexjunk/emoncms/alpine3.20_emoncms11.11.3
the build logs are fine

I have to test if ingress is allright

@alexandrecuer
Copy link
Copy Markdown
Contributor

alexandrecuer commented May 10, 2026

Just tested and it seems to work

image
For those of yours using home-assistant, you can also test, I've migrated the next branch to the last container 11.11.3

Add in the next branch to the store : https://github.com/Open-Building-Management/emoncms#next

image

Copy link
Copy Markdown
Contributor

@alexandrecuer alexandrecuer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems fine

Comment thread core.php
// FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE causes
// filter_var to return false for private/reserved ranges, true for public.
// So a private/loopback address returns false here, meaning is_trusted_proxy() = true.
return filter_var(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Public IP → filter_var() returns IP → === false → false

Private or reserved IP → filter_var() returns false → === false → true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REMOTE_ADDR trusted ?
127.0.0.1 yes
::1 yes
172.18.0.2 yes
192.168.1.10 yes
10.0.0.5 yes
IP publique Internet no

@TrystanLea
Copy link
Copy Markdown
Member Author

Thanks much appreciated, glad that's all working fine. Will merge and do a forum post alongside all the other already merged hardening changes 👍 I think the worst issue was a potential secondary SQL injection in the dashboard clone feature, that's patched now in latest release.

@alexandrecuer
Copy link
Copy Markdown
Contributor

@TrystanLea : once you merge, i will rebuild the container with all the versions of components fixed...just used master for the tests, but use specific versions is more clear for tracability.

@TrystanLea TrystanLea merged commit 8417025 into master May 11, 2026
0 of 14 checks passed
@TrystanLea
Copy link
Copy Markdown
Member Author

Thanks @alexandrecuer v11.12.1 is now released and available on stable

@alexandrecuer
Copy link
Copy Markdown
Contributor

@TrystanLea TrystanLea deleted the fix/trusted-proxy-host-header-injection branch May 12, 2026 19:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants