fix(security): idempotent /api/configure-pat (defence-in-depth follow-up to #42)#51
Merged
Merged
Conversation
PR #42 closed the impersonation vector by gating /api/configure-pat on the owner. That's the primary control: only the owner can submit a PAT. But the endpoint is still non-idempotent. Once a PAT is active, the owner (or anything driving the owner's browser — XSS, malicious tab, session-hijacking malware, or just an accidental double-submit) can POST a different PAT and the rotator will swap to it, revoking the previously-active token as a side effect. This adds an idempotency guard immediately after the owner check: if pat_rotator.token and not pat_rotator.is_token_expired: return 409 "PAT already configured. Restart the app to reconfigure." Bootstrap is single-shot by design — the rotator handles all subsequent credential refreshes automatically. There's no legitimate reason for the frontend to call configure-pat twice in one app lifetime (confirmed by grep — the only caller is the PAT-bootstrap path inside the "PAT not valid" branch of createPane()). The expired-token escape hatch is preserved: if the rotator times out because all sessions were reaped (long idle), is_token_expired returns True and the owner can re-bootstrap without an app restart. 409 Conflict (not 403 Forbidden) is the right status here: the issue isn't authorization (the owner DOES have permission) — it's that the operation conflicts with current state. Tests: - test_configure_pat_rejected_when_already_configured: rotator alive → even owner gets 409. Mocks pat_rotator.token + is_token_expired as PropertyMocks on the class (instance properties don't accept patching through mock.patch.object directly). - test_configure_pat_allowed_when_token_expired: rotator timed out → owner can still bootstrap (gets 400 "Token required" with empty body, proving they passed both gates). - All 6 TestConfigurePatAuth cases pass; 233/233 total. Co-authored-by: Isaac
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Defence-in-depth follow-up to the configure-pat hotfix in #42. Refuses re-configuration of the PAT once the rotator is alive with a valid token, returning 409 Conflict.
Why this exists
PR #42 closed the impersonation vector — a non-owner can no longer call `/api/configure-pat` and persistently impersonate the owner.
But the endpoint is still non-idempotent for the legitimate owner. Once a PAT is configured, anything that can drive the owner's authenticated browser session can swap PATs out from under the rotator:
The owner-gate from #42 cannot distinguish "the owner sent this" from "something running in the owner's browser sent this." This PR adds a state-based check that closes the swap vector regardless of who appears to be calling.
The guard
Why 409, not 403
403 says "you're not allowed." 409 says "the resource is in a state that conflicts with this request." The owner DOES have permission — the issue is that bootstrap is single-shot. 409 is the semantically correct response.
Tests
What this PR is NOT
Test plan
This pull request and its description were written by Isaac.