Skip to content

Security hardening: fail-closed auth, POST-only reactivation, tag endpoint fixes - #13

Merged
codeling merged 1 commit into
mainfrom
claude/security-hardening-auth-reactivation-tags
Oct 5, 2026
Merged

codeling merged 1 commit into
mainfrom
claude/security-hardening-auth-reactivation-tags

Conversation

@codeling

@codeling codeling commented Oct 5, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes the findings of a security review of the app. No critical vulnerabilities were found (SQL injection, XSS and POST CSRF were checked and are fine); the findings below were verified by reproducing them against a running instance before fixing.

Authentication / deployment

  • Fail closed without web server auth. The app has no login of its own and relied entirely on .htaccess/nginx. If .htaccess was ignored (e.g. AllowOverride None), everything was open. Requests without an authenticated REMOTE_USER are now rejected with a 403. Only REMOTE_USER is trusted, not the forgeable Authorization header. Opt out with $require_http_auth = false; in config.php.
  • HTTPS first. .htaccess requires HTTPS (or X-Forwarded-Proto: https) before the password prompt, so browsers never send credentials over plain HTTP. HSTS added to Apache and nginx samples; the nginx sample redirects to HTTPS. Session cookie gets Secure behind a TLS-terminating proxy.
  • Helper-script protection. The <If> rule on REQUEST_URI was bypassed by queries/db.php/x. Replaced with FilesMatch, which also covers path-info requests. recurrence.php is now blocked too.
  • nginx sample passes REMOTE_USER to PHP-FPM.

Data changes

  • No more writes on GET. query-todos.php ran reactivation of recurring entries (inserts) with no CSRF protection and for all users. It is now the POST endpoint reactivate-due.php (CSRF-protected, current user's lists only). Each entry is claimed in recurringCopied first inside a transaction, so concurrent requests cannot create duplicates. todo.js calls it before loading the list.
  • Merging a tag into itself deleted the tag and all its assignments. Now rejected server-side and no longer offered in the UI. Tag edit/delete/merge run in transactions.
  • Tag ownership. Tag edit/delete/merge only work on tags used by the current user's todos and by nobody else (no-op while single-tenant, safe once there are multiple users).
  • Parameter validation. A missing tag_name blanked the tag name; now rejected. Endpoints use postParam instead of raw $_POST.
  • Data endpoints answer with application/json.

Behaviour changes to be aware of

  • Auth is now required by default. Apache Basic auth works unchanged. nginx setups must pass REMOTE_USER (fastcgi_param REMOTE_USER $remote_user;, now in the sample). HTTP-only setups get a 403 until they use HTTPS or send X-Forwarded-Proto. Deployments with other protection must set $require_http_auth = false;. CI and the test docs do this.

Not included

  • Pinning shivammathur/setup-php to a commit SHA.
  • Moving config.php out of the webroot (no custom config path support; the sample config now recommends it).

Testing

  • npm test: 22/22 pass (API, Playwright UI, and new tests/auth.test.js, which starts its own PHP servers and needs no DB).
  • New regression tests cover each fix; I confirmed they fail on the original code and pass now.
  • tests/apache-access.sh extended (HTTPS-first, path-info cases, HSTS); confirmed it fails against the old .htaccess.
  • Manual end-to-end with real Apache + mod_php: REMOTE_USER reaches PHP (401 without credentials, 200 with), and with AllowOverride None the app now returns 403 instead of serving everything.
  • npm audit --omit=dev: 0 vulnerabilities; vendor/ matches the lockfile.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LeVfWCjSSQSgKKHpGbjKWK


Generated by Claude Code

Authentication
- Refuse requests the web server did not authenticate (no REMOTE_USER), so
  an ignored .htaccess or a missing nginx auth block fails closed instead of
  leaving all data open. Opt out with $require_http_auth = false in config.php.
- .htaccess/nginx: demand HTTPS before any password prompt, send HSTS, pass
  REMOTE_USER to PHP-FPM, and block helper scripts with FilesMatch, which
  also covers requests like queries/db.php/x (the <If> on REQUEST_URI did not).
- Session cookie gets the Secure flag behind a TLS-terminating proxy.

Data changes
- Reactivation of due recurring entries no longer happens as a side effect of
  GET query-todos.php (no CSRF protection, ran for all users): it is now the
  POST endpoint reactivate-due.php, limited to the lists of the current user,
  and claims each entry in recurringCopied first so concurrent requests cannot
  create duplicates.
- Merging a tag into itself deleted the tag and all its assignments: rejected
  on the server and no longer offered in the UI. Tag edit/delete/merge run in
  a transaction.
- Tag endpoints check ownership (tag used by the current user's todos and by
  nobody else) and validate their parameters; a missing tag_name no longer
  blanks the tag name.
- Data endpoints answer with application/json.

Tests: regression tests for each of the above, tests/auth.test.js for the
authentication check, extended tests/apache-access.sh.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeVfWCjSSQSgKKHpGbjKWK
@codeling
codeling merged commit 95c5558 into main Oct 5, 2026
11 checks passed
@codeling
codeling deleted the claude/security-hardening-auth-reactivation-tags branch October 5, 2026 20:47
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.

2 participants