Skip to content

fix(server): add loopback trust proxy mode - #377

Open
kkkzbh wants to merge 2 commits into
Waishnav:mainfrom
kkkzbh:feat/loopback-trust-proxy
Open

kkkzbh wants to merge 2 commits into
Waishnav:mainfrom
kkkzbh:feat/loopback-trust-proxy

Conversation

@kkkzbh

@kkkzbh kkkzbh commented Sep 28, 2026 •

Copy link
Copy Markdown

When DevSpace runs behind a tunnel on the same machine (cloudflared, Tailscale Funnel, a local nginx), every request reaches Express from 127.0.0.1 and the real client address only exists in X-Forwarded-For. server.trustProxy currently offers two choices, and neither works well for that setup. With false, every client shares one req.ip, so the rate limiter on the OAuth endpoints puts all callers in one bucket and anyone can exhaust the owner's /authorize budget. With true, Express takes the leftmost X-Forwarded-For entry, which the client controls, so sending a new value per request bypasses the limit on owner password attempts.

This adds "loopback" as a third value for server.trustProxy and passes it straight to Express's trust proxy setting. Express then trusts only hops appended by a loopback proxy, so req.ip resolves to the address the tunnel actually saw. Request logs use that same req.ip in loopback mode instead of reading cf-connecting-ip or X-Forwarded-For directly, so logs and rate limiting agree. true and false behave exactly as before, and the config schema, generated JSON schema, and configuration docs are updated.

The new logger.test.ts sends a spoofed leftmost hop through a real Express app in each mode, and config.test.ts covers accepting "loopback" and rejecting other strings.

Summary by CodeRabbit

  • New Features
    • Added a “loopback” proxy-trust option, which uses forwarded client IPs only when they come from a same-machine proxy. The existing settings remain: false ignores forwarding headers, while true trusts every hop.
    • The selected proxy-trust setting affects client IPs used in request logs and OAuth rate limiting.
  • Documentation
    • Expanded the configuration reference with proxy-trust options and their effects.

Accept "loopback" for server.trustProxy so Express only honors
X-Forwarded-For hops appended by a proxy on the same machine
(cloudflared or a local reverse proxy). With `true`, any client can pick
its own req.ip, which also keys the OAuth endpoints' rate limiting.

In loopback mode request logs use the IP Express resolves instead of
reading forwarding headers directly, so logs and rate limiting agree.
@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 2f453121-6a04-48e8-ab63-5a4d32d8f6ab

📥 Commits

Reviewing files that changed from the base of the PR and between d4d526c and 1d3546e.

📒 Files selected for processing (1)
  • src/logger.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

The server.trustProxy setting now accepts false, true, or "loopback". Express uses the configured value, and requestIp reads forwarding headers only when the value is true.

Changes

Proxy trust configuration and IP handling

Layer / File(s) Summary
Trust-proxy configuration contract
src/config-schema.ts, schema/v1/devspace.schema.json, src/config.test.ts, docs/configuration.md
The schemas accept "loopback" while retaining the false default. Tests cover "loopback" and reject "uniquelocal". The configuration reference describes the three modes and identifies request logs and OAuth rate limiting as uses of the derived client IP.
Express and logger IP resolution
src/logger.ts, src/server.ts, src/logger.test.ts
Express receives the configured trust-proxy value. requestIp reads Cloudflare and forwarded-for headers only when the value is true; otherwise, it uses req.ip and then the socket address. Integration tests compare the resolved request and logged IPs across the three modes.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Express
  participant requestIp
  Client->>Express: Send request with forwarded hops
  Express->>Express: Resolve req.ip using trustProxy
  Express->>requestIp: Pass request and trustProxy mode
  requestIp->>requestIp: Read forwarding headers only when trustProxy is true
  requestIp-->>Express: Return resolved IP
Loading

Merge Risk: ⚪ Minimal · up to 1d354

The new mode is carried consistently from configuration to Express and request logging. The reviewed changes show no concrete issue that should block merging.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 1d354

The new mode can distinguish callers behind a local tunnel without trusting every forwarded address. Its protection depends on the local proxy supplying a trustworthy client address, which the configuration does not enforce and the documentation does not spell out.

Retained concerns

  • Medium · security · inferred: The recommended loopback mode trusts a forwarded hop from any loopback-connected peer; it does not verify that the peer is the intended tunnel or that the tunnel supplied the actual remote address. If a local forwarding path preserves an attacker-controlled rightmost hop, the client IP used by downstream controls could be spoofed. This is a conditional trust-boundary risk, not a verified bypass in a deployed tunnel.
Security review details

Security Blast Radius

  • inferred — If the proxy-hop assumption fails, the affected identity is the client IP for requests reaching this server, including the documented OAuth rate-limit use; the evidence does not establish a broader tenant or service boundary change.

Security Findings and Attack Paths

  • inferred — A remote client can supply forwarding headers. If the local forwarding path fails to append or replace the actual remote address, a trusted loopback connection can make an attacker-supplied hop appear to be the client IP. Neither that deployment condition nor an OAuth rate-limit bypass was verified.

Trust Boundaries and Controls

  • observed — The loopback control delegates IP selection to Express and avoids the logger's direct raw-header handling. It distinguishes the spoofed leftmost hop in the supplied test, but does not authenticate the local proxy process.

Hardening Proposals

  • proposed — Document that loopback mode requires an exclusively trusted local forwarding path whose proxy appends or replaces the actual remote client address, and validate that property for supported tunnel configurations.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 5 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding the "loopback" trust proxy mode for the server.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit checks the forwarded trail,
With loopback set, it reads the local tale.
True lets headers guide the way,
False keeps those hints at bay.
Logs record the IPs it sees,
And hops along among the trees.

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

[Medium risk] Adds loopback mode to request IP trust configuration.

Safe to merge with a non-blocking gap in regression coverage.

Findings

  1. P2 Test spoofed CF headers ▶

Summary

The PR adds a loopback trust-proxy option so operators behind a local proxy can use the trusted client address for request logs and OAuth rate limiting. It also updates configuration guidance and adds IP-resolution tests. The current loopback logger uses Express’s trusted IP, but its test does not check a conflicting CF-Connecting-IP header. This non-blocking test gap leaves a future logging regression undetected.

Reviews (1) · Last reviewed commit: "fix(server): add loopback trust proxy mo..."

Comment thread src/logger.test.ts Outdated
const { port } = server.address() as AddressInfo;
// The client supplies the leftmost hop; the local proxy appends the real peer.
const response = await fetch(`http://127.0.0.1:${port}/`, {
headers: { "x-forwarded-for": "198.51.100.66, 203.0.113.7" },

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.

P2 Test spoofed CF headers

The loopback test sends only X-Forwarded-For, so it still passes if logging begins preferring a client-supplied CF-Connecting-IP over Express’s trusted req.ip. That leaves a logging regression undetected. Send a conflicting CF header and assert that the logged IP remains req.ip. This test-coverage concern does not block merging.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Artifacts

Executed test and mutation command

  • This script creates isolated source copies, applies the loopback-only mutation, and runs the unchanged tests and HTTP probe against each copy, showing the exact executed setup.

Source of the spoofed-header HTTP request

  • This executed probe sends both forwarding headers and captures the HTTP response and request-log IP, showing how the mismatch was measured.

Original code: passing tests and matching IPs

  • The unchanged test passed 3/3 and the HTTP 200 OK request logged the same IP as req.ip, establishing the baseline.

Mutated code: passing tests despite spoofed logged IP

  • The unchanged test still passed 3/3, while the HTTP 200 OK request logged the spoofed CF IP instead of req.ip, confirming the coverage gap.

View artifacts

T-Rex Ran code and verified through T-Rex

@greptile-apps

greptile-apps Bot commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

Comments Outside Diff

These findings sit on lines the diff does not cover, so they could not be posted inline. Each one leaves this list once its file changes.

  • P2 Loopback logging test misses spoofed CF-Connecting-IP ▶

    • Bug
      • The unmodified loopback test passes when logging is changed to prefer a spoofed CF-Connecting-IP over req.ip. A runtime request demonstrates the resulting mismatch.
    • Cause
      • src/logger.test.ts:20 supplies no CF-Connecting-IP header, so its assertion at line 33 never exercises precedence between that header and Express's trusted req.ip.
    • Fix
      • Add a conflicting client-supplied CF-Connecting-IP to the loopback request and assert that the logged IP remains the trusted req.ip.

@kkkzbh

kkkzbh commented Sep 28, 2026

Copy link
Copy Markdown
Author

Following up on the automated reviews:

  • Greptile's test gap is addressed in 1d3546e: the logger test now sends a conflicting client-supplied CF-Connecting-IP alongside the spoofed X-Forwarded-For hop, and loopback mode must still log the trusted req.ip. Letting loopback read the CF header again makes that test fail.
  • On CodeRabbit's retained concern about the OAuth limiter: mcpAuthRouter is mounted without a custom keyGenerator, and express-rate-limit 8.5.2 keys on request.ip by default, so it counts against the same address loopback mode resolves.
  • Checked on a real cloudflared deployment as well: a request sent through the tunnel with a spoofed X-Forwarded-For: 6.6.6.6 was logged with the client's actual address, and Cloudflare rejects client-supplied CF-Connecting-IP with a 403 before it reaches DevSpace.

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.

1 participant