Skip to content

Security: bind a stored credential to the request it was saved for - #34

Merged
SandObserver merged 1 commit into
mainfrom
security/scope-imported-secrets
Aug 11, 2026
Merged

SandObserver merged 1 commit into
mainfrom
security/scope-imported-secrets

Conversation

@SandObserver

Copy link
Copy Markdown
Owner

Summary

  • Apply the existing secret-scope check to POST /api/config. A withheld secret is restored only when everything about that request other than the secret is unchanged, which is the rule the badge-proxy and widget-test endpoints already enforce.
  • Judge an app's two blocks separately, so changing where the activity badge points does not also drop the credential the status badge uses.
  • Return the items whose credential was withheld and report them in the admin UI, so a save that moved a request does not appear to have kept working.
  • Record the withheld ids in the audit log.
  • Document the rule in docs/security.md, which previously said secrets are preserved on save without qualification.

Testing
All green: 1098 backend, 618 frontend, 94.8% line coverage, lint, format, both typechecks, paths:check and cache-busting. Added 6 tests covering both secret systems, the legitimate restore, and an item that never held a secret.

Verified against a container before and after. Before: a config keeping an item's id and its secret marker while changing the URL caused the server's next poll to send Authorization: Bearer <stored token> to a listener on the test network, for both a badge header and a widget config key. After: the value is not restored, the response is {"ok":true,"withheld":[{"id":"b1","label":"Badge App"}]}, the poll leaves with an empty header, and exporting then re-importing still keeps the token.

Checklist

  • Stays within the project constraints: one container, no runtime dependencies, no build step (see CONTRIBUTING.md).
  • Tests added or updated for new logic, and the full suite, lint, and typechecks pass.
  • Docs updated if behavior or configuration changed.
  • CHANGELOG.md updated under Unreleased if the change is user-visible.

Refilling a withheld secret matched the incoming item by id alone, so a
saved config could keep an item's id and its secret marker, point the
request at another host, and have the server fill the real credential back
in. The next poll then delivered it there, with nothing on screen to show
for it. A config file is something people import from elsewhere, which
makes the file the attack.

The scope check that already guards the two endpoints for testing a badge
or a widget now guards the save path as well: a secret is restored only
when everything about that request other than the secret is unchanged. The
two blocks on an app are judged separately, so moving the activity badge
does not drop the credential the status badge uses.

Anything withheld is named in the response and reported in the admin UI,
so a save that moved a request does not look like it kept working.
Restoring an export is unaffected: it changes nothing but the values.
@SandObserver SandObserver added the security Security fix label Aug 11, 2026
@SandObserver
SandObserver merged commit 1837b4c into main Aug 11, 2026
4 checks passed
@SandObserver
SandObserver deleted the security/scope-imported-secrets branch August 11, 2026 21:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

security Security fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant