The Hollow Courier
Summary
This challenge was a proxy trust misconfiguration that exposed an internal-only route.
The vulnerable endpoint was POST /app/gate/decree. It was intended to be reachable only by the application’s internal watch relay, but the Flask app trusted too many X-Forwarded-For hops. That allowed an external client to spoof an internal source address and bypass the internal-only check.
The fix was to reduce Flask’s trusted proxy hop count from x_for=2 to x_for=1, which matches the actual deployment topology in this repository.
How We Found It
We approached the challenge by reading the route definitions and the tests together.
1. Identify security-sensitive routes
The first place to inspect was checkpoint/app/routes.py. That file documents the route surface clearly:
POST /app/gate/presentGET /app/gate/inspectPOST /app/gate/decree
The important part was this comment and guard around decree():
- “Inner desk: seal a binding crown decree for the watch.”
if not gate.require_internal(): abort(403)
That immediately marked /app/gate/decree as the most security-sensitive endpoint in the application, because it is not supposed to be public.
2. Inspect how “internal” is decided
Next we read checkpoint/app/gate.py.
The check is based on request.remote_addr, which is accepted as internal when it falls into one of these ranges:
10.0.0.0/8172.16.0.0/12192.168.0.0/16127.0.0.2/32
By itself that is not necessarily wrong, but it means the correctness of the access control depends entirely on whether request.remote_addr is reconstructed safely behind proxies.
3. Inspect the proxy boundary
Then we read checkpoint/conf/Caddyfile and checkpoint/app/__init__.py.
The Caddy config shows a single reverse proxy hop:
- Caddy listens on
:8000 - It proxies to
127.0.0.1:5000
The Flask app uses:
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=2, x_proto=1, x_host=1)
That was the key mismatch.
There is only one trusted proxy in this repository’s deployment path, but the app was trusting two X-Forwarded-For entries. In Werkzeug ProxyFix, over-trusting proxy hops lets an attacker control which earlier address in the forwarding chain becomes request.remote_addr.
That means a client can send a forged X-Forwarded-For header containing an internal-looking address and trick the app into treating the request as internal.
Root Cause
The root cause was incorrect trust of proxy-supplied client IP metadata.
The application assumed this topology:
- trusted proxy 1
- trusted proxy 2
- application
But the repository actually deploys:
- Caddy
- application
With x_for=2, the app trusted one hop too many. That makes the left side of the forwarded chain partially attacker-controlled.
In practical terms, an external request could present a header like:
X-Forwarded-For: 127.0.0.2, 198.51.100.9
and, because the app trusted two forwarded addresses, request.remote_addr could resolve to the internal alias 127.0.0.2 instead of the real external client.
That breaks the guarantee around:
gate.require_internal()- internal-only access to
/app/gate/decree
How We Solved It
We changed the proxy trust configuration in checkpoint/app/__init__.py.
Before
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=2, x_proto=1, x_host=1)
After
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)
We also updated the comment above that line to document the reasoning:
- the service sits behind a single trusted proxy hop
- trusting more forwarded entries enables origin spoofing
This is the smallest correct fix because it preserves the intended use of proxy headers while aligning trust with the real deployment.
How We Tested It
We used two levels of validation.
1. Add a regression test
We added a test in checkpoint/tests/test_gate.py:
def test_decree_rejects_spoofed_forwarded_for_chain(client):
response = client.post(
"/app/gate/decree",
json={"order": "bar the south rail"},
environ_overrides={"REMOTE_ADDR": "127.0.0.1"},
headers={"X-Forwarded-For": "127.0.0.2, 198.51.100.9"},
)
assert response.status_code == 403
This test models the actual issue:
- the app receives a request through the local proxy path
- the client supplies a forged forwarded chain
- the route must still reject the request as external
Before the fix, this kind of input could be interpreted as internal. After the fix, it is denied.
2. Run the full test suite
We created a local virtual environment, installed the challenge dependencies, and ran the checkpoint tests with:
cd checkpoint
python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements-dev.txt
python -m pytest -q
Result:
26 passed in 1.18s
This matters because the challenge explicitly requires preserving normal behavior. Running the full test suite verified that:
- public routes still work
- staff and role-gated pages still work
- writ verification still works
- the new security behavior does not break the rest of the checkpoint
Why This Fix Is Correct
This fix is correct for both security and functionality.
Security
- It prevents attacker-controlled
X-Forwarded-Forvalues from being over-trusted. - It restores the integrity of
request.remote_addr. - It protects the internal-only route from external spoofing.
Functionality
- The application still understands proxy headers from the one real perimeter proxy.
- No route logic changed except for the trust boundary interpretation.
- All existing tests still pass.
Final Patch
Files changed:
checkpoint/app/__init__.pycheckpoint/tests/test_gate.py
Commit used for submission:
bc4bbae-Fix internal route proxy trust
Short Version
We found the bug by tracing the internal-only route, then following how client origin was reconstructed behind the proxy. The app trusted two forwarded IP hops even though the repo’s deployment only has one trusted proxy. That allowed external origin spoofing through X-Forwarded-For and exposed /app/gate/decree. We fixed it by changing ProxyFix(..., x_for=2) to ProxyFix(..., x_for=1), added a regression test for spoofed forwarded chains, and verified the patch with the full test suite.