Skip to content

fix(security): throttle failed logins; add security headers to the API - #135

Merged
snevesbarros merged 1 commit into
masterfrom
claude/security-hardening
Oct 3, 2026
Merged

snevesbarros merged 1 commit into
masterfrom
claude/security-hardening

Conversation

@snevesbarros

Copy link
Copy Markdown
Collaborator

Summary

Fixes two findings from the security review: no login rate limiting, and no clickjacking or security headers. The UI half of the headers fix is in AnglesHQ/angles-ui (same branch name).

Login throttling (app/utils/login-throttle.js)

Before: the local and LDAP credential logins had no limit. 50 wrong passwords against admin took under 5 seconds, and the correct password still worked straight after.

Now: failed sign-ins are counted within a 15-minute window. Once over a limit, the login answers 429 with a Retry-After header.

Limit Default Stops
Per client IP + username (case-insensitive) 5 guessing one account's password
Per client IP, across usernames 50 spraying guesses at many accounts
  • No per-username lock, deliberately: it would let anyone lock any account (admin included) out just by failing to sign in as it. Other clients can always still sign in.
  • Success resets: a successful sign-in clears that client's count for the username.
  • Configuration: ANGLES_LOGIN_MAX_FAILURES, ANGLES_LOGIN_MAX_FAILURES_PER_IP and ANGLES_LOGIN_LOCKOUT_MINUTES.
  • Limitations:
    • Counts are kept in memory, so each API instance limits on its own and a restart clears them.
    • Behind a reverse proxy, set TRUST_PROXY=true so the client's real address is used. Otherwise every request comes from the proxy and shares the per-IP limit.

Security headers (app/utils/security-headers.js)

  • Every API response now sends:

    • X-Content-Type-Options: nosniff
    • X-Frame-Options: DENY
    • Referrer-Policy: no-referrer
    • Content-Security-Policy: default-src 'none'; frame-ancestors 'none'. The API answers with JSON and files, so a response that ever gets rendered as a page can't load or run anything.

    X-Powered-By is no longer sent.

  • Swagger UI (/api-docs) gets frame-ancestors 'none' only, so its own scripts and styles keep working.

  • HTML build report gets its own policy: inline styles, data: images, and only its single inline script, allowed by a per-request nonce.

  • Attachment files keep the sandbox policy they already set.

Testing

  • New test/security-hardening.tests.js (11 tests):
    • throttle: the per-pair limit (refused even with the right password), no lock for other clients or other accounts, usernames counted case-insensitively, the count clearing on success, the per-IP limit, window expiry, and the LDAP route being guarded
    • headers: on API responses and error responses, Swagger, and the report's nonce (unique per request and matching the script tag)
  • Full suite against a local MongoDB: 560 passing (549 before, plus 11 new). npx eslint app server.js is clean. The existing suite's deliberate failed logins don't trip the limits.
  • Opened the HTML report from the API in Chromium: its script runs under the nonce policy ("Expand all" works) with no CSP violations.

Notes

🤖 Generated with Claude Code

https://claude.ai/code/session_01KgQXSUjuLVXmLWxobnMfSf


Generated by Claude Code

Login throttling: the local and LDAP credential logins had no limit (50
wrong passwords against admin took under 5 seconds, and the right one still
worked straight after). A new in-memory throttle counts failed sign-ins in a
15-minute window and answers 429 with Retry-After:
- 5 failures per client IP + username (case-insensitive), and
- 50 failures per client IP across usernames.
There is deliberately no per-username lock, which would let anyone lock an
account (e.g. admin) out by failing to sign in as it. A successful sign-in
clears that client's count. The limits and window are configurable with
ANGLES_LOGIN_MAX_FAILURES, ANGLES_LOGIN_MAX_FAILURES_PER_IP and
ANGLES_LOGIN_LOCKOUT_MINUTES. Behind a reverse proxy, TRUST_PROXY=true is
needed for the client's real address to be used.

Headers: every API response now carries X-Content-Type-Options: nosniff,
X-Frame-Options: DENY, Referrer-Policy: no-referrer and
Content-Security-Policy "default-src 'none'; frame-ancestors 'none'", and
X-Powered-By is no longer sent. The Swagger UI keeps its scripts (CSP
frame-ancestors only). The HTML build report gets its own policy: inline
styles, data: images, and only its one script, by a per-request nonce.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KgQXSUjuLVXmLWxobnMfSf
@snevesbarros
snevesbarros merged commit 224f837 into master Oct 3, 2026
1 check passed
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