fix(security): block PasswordHash changes via config API#271
Conversation
POST /web/config and /api/config accepted WebUI.PasswordHash="", which cleared the stored hash while leaving WebUI.Token valid. An attacker with only the API token could then call set-password in bootstrap mode and take over the admin account. Reject all direct PasswordHash writes through config endpoints (except the [redacted] placeholder). Password changes must use POST /web/auth/set-password. Co-authored-by: Feramance <Feramance@users.noreply.github.com>
There was a problem hiding this comment.
Critical bypass found in PR #271
The dotted-key guard (RejectPasswordHashConfigChange) is correct for the paths it covers, but two config API shapes still bypass it and can clear PasswordHash, reopening bootstrap POST /web/auth/set-password with only the API token.
Bypass 1: Full /api/config replace
POST /api/config without a changes wrapper deserializes a full TorrentarrConfig and never calls RejectPasswordHashConfigChange:
else
{
updatedConfig = JsonConvert.DeserializeObject<TorrentarrConfig>(payload.GetRawText());
}Trigger: Authenticated caller sends POST /api/config with WebUI.PasswordHash: "" in a full config body → 200 OK → set-password with setupToken: <api-token> succeeds.
Bypass 2: Section-level WebUI replace
ApplyDottedConfigChanges only checks dotted keys. A top-level WebUI object in changes replaces the entire section:
{"changes": {"WebUI": {"PasswordHash": "", ...}}}Trigger: Same account-takeover chain as above; confirmed with integration test returning 200 before fix.
Recommended fix
Add a save-time guard (ValidatePasswordHashForConfigApiSave) that rejects any PasswordHash change on config API persistence, regardless of payload shape. Implemented on branch cursor/critical-bug-investigation-82da.
What looks good
- Dotted-key rejection for
""and arbitrary hash values [redacted]placeholder handling for normal frontend round-trips- Regression tests for the originally reported dotted-key attack
Sent by Cursor Automation: Torrentarr - Find critical bugs
There was a problem hiding this comment.
PR Validation (Cursor Automation)
Recommendation: Close (incorrect fix)
Primary reason: The PR blocks WebUI.PasswordHash only for dotted changes payloads, but POST /api/config still accepts a full JSON replacement payload that can set or clear WebUI.PasswordHash without going through POST /web/auth/set-password.
Gates
| Gate | Status | Notes |
|---|---|---|
| Merge conflicts | Pass | origin/cursor/critical-bug-investigation-7357 merged cleanly with origin/master; no conflicts. |
dotnet build |
Pass | dotnet build -c Release passed with 0 errors and 6 existing NU1510 warnings. |
dotnet test (non-live) |
Pass | dotnet test -c Release --no-build --filter "Category!=Live" passed: 745 passed, 0 failed. |
vitest |
Pass | npm ci && npx vitest run passed: 16 files, 154 tests. |
Validation
| Axis | Score | Notes |
|---|---|---|
| Purpose | Pass | Direct config changes to WebUI.PasswordHash are a real auth/config issue and the PR is aimed at the correct surface. |
| Correctness | Fail | ApplyDottedConfigChanges rejects WebUI.PasswordHash, but the no-changes branch in POST /api/config still deserializes and saves a full TorrentarrConfig, leaving a direct PasswordHash write path. This also fails the Torrentarr-specific check that POST /api/config should use dotted changes merge rather than full JSON replace. |
| Tests | Fail | Added tests cover /web/config and /api/config dotted changes, but not the remaining full-payload /api/config bypass. |
| Hygiene | Pass | Focused 5-file change with no generated SDK junk or unrelated noise. |
| Overlap | Pass | Not treating as a duplicate: the prompt’s 2026-06-15 winner table names #271 for Config PasswordHash security. #258 overlaps broader /api/config dotted-only work but is not the preferred focused PasswordHash winner. |
Why
The merge/build/test gates are green, but the security invariant is still not enforced globally: WebUI.PasswordHash can still be supplied through the full-replacement POST /api/config path because that branch bypasses the new RejectPasswordHashConfigChange helper. The fix should either remove/disable full replacement for /api/config in favor of dotted changes, or explicitly reject WebUI.PasswordHash changes before saving any full config payload.
Required repo context was checked. Note: docs/audits/pr-triage-2026-06-15.md was not present in the checkout; the latest available audit file was docs/audits/pr-triage-2026-06-11.md, and the trigger prompt’s 2026-06-15 winner table was used for overlap guidance.
Overlap
None as a duplicate recommendation. Related broader config PR: #258.
Commands run
git fetch origin master; git fetch origin cursor/critical-bug-investigation-7357; git checkout -B pr-validate origin/cursor/critical-bug-investigation-7357; git merge origin/master; git diff --stat origin/master...HEAD; gh pr list --state open --search "PasswordHash repo:Feramance/Torrentarr"; gh pr view 258 --json ...; temporary /tmp/dotnet SDK install because dotnet was initially missing; dotnet restore; dotnet build -c Release; dotnet test -c Release --no-build --filter "Category!=Live"; npm ci; npx vitest run; git status --short --branch; git diff --exit-code.
Sent by Cursor Automation: Torrentarr PR validation triage
- Keep #258 dotted-only POST /api/config (require changes field) - Use #271 RejectPasswordHashConfigChange (403) for PasswordHash writes - Align SetPasswordEndpointTests with 403 on empty PasswordHash clear - Bump NewtonsoftJson to 10.0.9 Co-authored-by: Feramance <Feramance@users.noreply.github.com>
* docs: Cursor Automation spec for PR triage on open/push Co-authored-by: Feramance <Feramance@users.noreply.github.com> * docs: one-shot PR triage audit report for open pull requests Co-authored-by: Feramance <Feramance@users.noreply.github.com> * feat: add repo-managed Cursor Automation definition for PR triage - .cursor/automations/torrentarr-pr-triage/ (automation.yaml + prompt.md) - Setup helper script and updated automation docs - Include pr-triage-2026-06-15.md ground-truth audit on branch Co-authored-by: Feramance <Feramance@users.noreply.github.com> * [pre-commit] auto fixes from pre-commit hooks for more information, see https://pre-commit.ci * docs: add one-click Cursor Automation editor launcher Bootstrap from find-vulnerabilities template (PR opened/pushed + PR comment). HTML launcher copies Torrentarr prompt for paste into editor. Co-authored-by: Feramance <Feramance@users.noreply.github.com> * Rewrite PR automation as validation (merge/close), not security scan - Prompt: merge master, resolve conflicts, run build/tests, post Merge or Close - Remove find-vulnerabilities bootstrap; use blank automations editor - Rename display to Torrentarr PR Validation in docs and scripts Co-authored-by: Feramance <Feramance@users.noreply.github.com> * Add automated PR actions + gh one-liners for maintainer follow-up - Prompt: push conflict fixes, gh pr ready on Merge, Maintainer commands section - New docs/audits/pr-triage-gh-actions.md with phased gh commands - Marked #229 #271 #258 #243 ready for review via gh Co-authored-by: Feramance <Feramance@users.noreply.github.com> * Simplify maintainer workflow to merge/close only - Automation auto-closes duplicates and does all prep (rebase, push, ready) - pr-triage-gh-actions.md: merge/close queue only - Add scripts/pr-queue.sh for live merge/close command list Co-authored-by: Feramance <Feramance@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Feramance <Feramance@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>


Summary
Fixes a critical auth bypass where anyone with
WebUI.Tokencould take over a configured admin account.Bug and impact
An attacker with only the API token (from
config.toml, logs, orGET /api/token) could:POST /web/configorPOST /api/configwith{"changes": {"WebUI.PasswordHash": ""}}to clear the stored password hashPOST /web/auth/set-passwordusingWebUI.Tokenas the bootstrap setup tokenThis bypassed the hardening in #225/#228 that restricted
set-passwordwhen a password was already configured.Root cause
ApplyDottedConfigChanges/POST /web/configonly skipped[redacted]placeholder values for sensitive keys. An empty string was accepted and persisted, re-opening bootstrap mode without requiringTORRENTARR_SETUP_TOKEN.Fix
WebUI.PasswordHashwrites via config endpoints (except the[redacted]placeholder)POST /web/auth/set-passwordValidation
dotnet test tests/Torrentarr.Infrastructure.Tests/ --filter FullyQualifiedName~WebUIAuthHelpersTestsdotnet test tests/Torrentarr.Host.Tests/ --filter FullyQualifiedName~ConfigRedactionTests