Skip to content

fix(tomcat): correct RemoteIpValve internalProxies regex and add CGNAT (#35804)#35805

Merged
wezell merged 4 commits into
mainfrom
issue-35804-ip-valve-regex
May 22, 2026
Merged

fix(tomcat): correct RemoteIpValve internalProxies regex and add CGNAT (#35804)#35805
wezell merged 4 commits into
mainfrom
issue-35804-ip-valve-regex

Conversation

@yolabingo
Copy link
Copy Markdown
Member

@yolabingo yolabingo commented May 22, 2026

Summary

  • Simplify regex escaping in setenv.sh — single \ (readable, equivalent in sh/bash double quotes)
  • Consolidate 10.*/127.* and 172.16-31 alternations into grouped patterns
  • Add CGNAT 100.64.0.0/10 (RFC 6598) range — used by cloud load balancers, Kubernetes overlay networks, and Tailscale.
  • Fix pre-existing Tomcat IntrospectionUtils.replaceProperties() bug in server.xml: bare {/} in \d{1,3} broke property resolution (first } closed ${...} prematurely). Replaced with \d\d?\d? — regex-equivalent, brace-safe

Test plan

The RemoteIpValve processes the X-Forwarded-For header and replaces request.getRemoteAddr() with the real client IP — but only when the immediate connecting IP matches internalProxies. Validation must confirm Tomcat's resolved remote IP, not just header presence.

Setup

  1. Deploy dotCMS without setting CMS_REMOTEIP_INTERNALPROXIES (use the default regex)
  2. You need a way to see what request.getRemoteAddr() returns after the valve processes the request. Options:
    • Access log (easiest): the default access log pattern uses %{org.apache.catalina.AccessLog.RemoteAddr}r — this logs the IP after RemoteIpValve processes it. Compare against %a (raw remote addr) or add both to the pattern
    • /api/v1/appconfiguration or any endpoint that echoes the client IP in its response/logs
    • Temporary VTL: $request.getRemoteAddr() in a page to see the resolved IP directly

Test cases

  • RFC 1918 proxies trusted — send request through 10.x.x.x, 192.168.x.x, or 172.16-31.x.x proxy with X-Forwarded-For: <real-client-ip>. Verify access log / getRemoteAddr() shows <real-client-ip>, not the proxy IP
  • Loopback trusted — request via 127.0.0.1 with XFF header. Verify real client IP resolved
  • IPv6 loopback trusted — request via 0:0:0:0:0:0:0:1 (::1) with XFF header. Verify real client IP resolved
  • CGNAT trusted — request via 100.64.x.x through 100.127.x.x with XFF header. Verify real client IP resolved
  • Link-local trusted — request via 169.254.x.x with XFF header. Verify real client IP resolved
  • External IP NOT trusted — request from a public IP (e.g., 203.0.113.1) with X-Forwarded-For: 1.2.3.4. Verify getRemoteAddr() still shows 203.0.113.1 (valve should NOT substitute)
  • CGNAT boundary100.63.255.255 should NOT be trusted (below range). 100.128.0.0 should NOT be trusted (above range)
  • Env var override — set CMS_REMOTEIP_INTERNALPROXIES to a custom regex. Verify custom value takes precedence over the default

Quick Docker test

# Start dotCMS with default config
docker run -p 8080:8080 dotcms/dotcms:latest

# Simulate proxy request (curl connects from 127.0.0.1 = loopback = trusted)
curl -H "X-Forwarded-For: 203.0.113.42" http://localhost:8080/api/v1/appconfiguration

# Check access log for resolved IP
docker exec <container> cat /srv/dotserver/tomcat-*/logs/dotcms_access*.log
# Should show 203.0.113.42 (the XFF value), not 127.0.0.1

Closes #35804

🤖 Generated with Claude Code

This PR fixes: #35804

… coverage (#35804)

- Simplify regex escaping in setenv.sh (single \ for readability)
- Consolidate 10.*/127.* and 172.16-31 alternations
- Add CGNAT 100.64.0.0/10 (RFC 6598) range for cloud/K8s proxies
- Replace \d{1,3} with \d\d?\d? in server.xml to avoid Tomcat
  IntrospectionUtils bare-brace parsing bug (pre-existing)
- Retain IPv6 loopback (0:0:0:0:0:0:0:1) in both files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the Area : Backend PR changes Java/Maven backend code label May 22, 2026
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Aligns with Tomcat 9/10.1 default which includes both expanded
(0:0:0:0:0:0:0:1) and compressed (::1) IPv6 loopback forms.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment between attributes is invalid per XML 1.0 STag grammar —
Xerces/Digester would reject it. Folded into existing comment block.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dotCMS dotCMS deleted a comment from claude Bot May 22, 2026
Copy link
Copy Markdown
Member

@wezell wezell left a comment

Choose a reason for hiding this comment

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

Nice work Todd

@wezell wezell enabled auto-merge May 22, 2026 02:31
@wezell wezell added this pull request to the merge queue May 22, 2026
Merged via the queue into main with commit 3eb213a May 22, 2026
52 checks passed
@wezell wezell deleted the issue-35804-ip-valve-regex branch May 22, 2026 04:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Safe To Rollback Area : Backend PR changes Java/Maven backend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

fix(tomcat): Regex escaping error in RemoteIpValve internalProxies + missing CGNAT

2 participants