Skip to content

fix(security): drop PASSWORD_SECRET from proxy env + add slowloris read-timeout (#101 P0) - #102

Merged
axisrow merged 2 commits into
mainfrom
ao/clihost_af28bd122d-24/pr-a-security-p0
Jul 8, 2026
Merged

fix(security): drop PASSWORD_SECRET from proxy env + add slowloris read-timeout (#101 P0)#102
axisrow merged 2 commits into
mainfrom
ao/clihost_af28bd122d-24/pr-a-security-p0

Conversation

@axisrow

@axisrow axisrow commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Summary

Two P0 unauthenticated-security fixes from the multi-agent code review in #101.

  • Add Claude Code GitHub Workflow #1PASSWORD_SECRET leaked into the proxy's environment (entrypoint.sh). runuser (without --login) inherits the caller's whole environment, so an operator-supplied -e PASSWORD_SECRET=... landed in the proxy's /proc/<pid>/environ — readable by any TTYD_USER process (including terminal shells) and enough to forge session/CSRF tokens. unset PASSWORD_SECRET after writing the secret file and before launching the proxy makes the mode-400 file the only channel, as CLAUDE.md guarantees.
  • Add build script for npm package cache invalidation #2 — slowloris DoS: no socket read timeout (app/ttydproxy). BaseHTTPRequestHandler had no read timeout, so a slow client pinned a ThreadingHTTPServer worker forever (the login rate-limiter only fires after the slow window). Added a config-driven REQUEST_TIMEOUT (default 30s, min 1) applied as the handler's timeout class attribute.

#101 findings closed

New/changed env vars

  • REQUEST_TIMEOUT — per-connection socket read timeout in seconds (default 30, min 1). Read in config.py, validated via env_int. Documented in .env.example by PR-B (which owns that file in this split).

Note: finding #2 targets app/server.py in the review, but the fix lives in config.py + app.py (the handler subclass carries the timeout attribute) so the value stays env-configurable and validated, rather than editing the stdlib-only server.py.

Ports / volumes

No changes.

Tests

New: test_password_secret_unset_before_proxy_launch (ordering assertion in test_shell_scripts.py), REQUEST_TIMEOUT config + handler-attribute tests in test_config_timeouts.py. Full pytest tests/ green; bash -n entrypoint.sh clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KgvJhzd8gEhXVE4MVu18nn

…ad-timeout

Two P0 unauthenticated-security fixes from the #101 code review.

#1 (entrypoint.sh): `runuser` (without --login) inherits the caller's whole
environment, so an operator-supplied `-e PASSWORD_SECRET=...` landed in the
proxy's /proc/<pid>/environ — readable by any TTYD_USER process and enough to
forge session/CSRF tokens. `unset PASSWORD_SECRET` after writing the secret file
and before launching the proxy makes the mode-400 file the only channel, as
CLAUDE.md guarantees.

#2 (app/ttydproxy): BaseHTTPRequestHandler had no read timeout, so a slow client
(slowloris) pinned a ThreadingHTTPServer worker forever (the login rate-limiter
only fires after the slow window). Add a config-driven REQUEST_TIMEOUT (default
30s, min 1) applied as the handler's `timeout` class attribute. Implemented in
config.py + app.py (the handler subclass) rather than the stdlib-only server.py
so the value stays env-configurable and validated.

Closes #101 findings #1, #2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KgvJhzd8gEhXVE4MVu18nn
@axisrow

axisrow commented Jul 8, 2026

Copy link
Copy Markdown
Owner Author

🔍 Local review (cycle 1)

Reviewed locally (Claude subagent + Codex companion), no bots pinged. Both reviewers ran; findings triaged against the real code + git history.

Verdict Reviewer Finding Location
SKIP codex Proxy signing secret still readable by a same-UID terminal shell entrypoint.sh:466-481
SKIP codex Slowloris timeout is per-read (idle), not a total header deadline app/ttydproxy/app.py:90-94

No FIX verdicts. Rationale (both findings are technically accurate but describe threats broader than the narrowly-scoped #101 P0 this PR closes):

Both are recorded as valid follow-up hardening ideas. This completes the local review cycle for #102 — merge is left to the maintainer.

Non-blocking review nitpick from the local review of #102: the timeout test
only asserted `> 0`. Also assert `TTYDProxyHandler.timeout == config.REQUEST_TIMEOUT`
so a regression that hard-codes or drops the config wiring is caught.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KgvJhzd8gEhXVE4MVu18nn
@axisrow

axisrow commented Jul 8, 2026

Copy link
Copy Markdown
Owner Author

Applied the deferred non-blocking test-strength nitpick in a4dc665 (assert the handler timeout is wired to config.REQUEST_TIMEOUT, not merely positive). Local review cycle for #102 is complete.

@axisrow
axisrow merged commit 1163aba into main Jul 8, 2026
3 checks passed
@axisrow
axisrow deleted the ao/clihost_af28bd122d-24/pr-a-security-p0 branch July 8, 2026 14:51
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