Clear the 1Password service-account token from the op-js global after each call - #1574
Open
GeiserX wants to merge 2 commits into
Open
Clear the 1Password service-account token from the op-js global after each call#1574GeiserX wants to merge 2 commits into
GeiserX wants to merge 2 commits into
Conversation
…t is done op-js keeps the service-account token on a module-level CLI instance and reads it when spawning `op`. The CLI backend set that global before each call and nothing cleared it, so a token handed over for one secret resolution stayed readable for the rest of the process's life, with nothing left to read it. The account-name branch blanks it, but only if a differently-authenticated call comes next -- which in a service-account-only deployment never happens. Clear it on success, failure and interruption alike. Every read and write of that global already happens inside the backend's semaphore, so the next operation re-sets the token before it spawns anything.
This was referenced Aug 13, 2026
Author
|
Context for this one: #1585 explains why this PR and twelve others exist — they came out of a single pass over credential handling, asking for each credential where it ends up, how long it stays, and who can read it once it's there. This PR stands alone and doesn't depend on any of the others. |
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.
TL;DR
Hygiene, not a security fix — and I want to be precise about which, because my first draft of this description overstated it.
@1password/op-jskeeps the service-account token on a module-level global (cli.serviceAccountToken) and reads it when it spawnsop. The CLI backend set that global before each call and never cleared it, so one reachable reference to the token stayed live for the rest of the process. This clears it once the call that needed it is done.What this does not do: it does not reduce what an attacker can reach. See "Why this is smaller than it looks" below. I'd rather say that up front than have you find it in review.
The change
makeCliServicesets the global before each call. Now it also clears it afterwards, on success, failure and interruption alike, viaEffect.ensuringinside the existing semaphore.Authentication is unaffected: every read and write of that global already happens inside
cliAuthLock, so the next operation re-sets the token before it spawns anything.Why this is smaller than it looks
Three reasons a reviewer should weigh before spending time on this:
opchild ever received the stale token.@1password/op-jsis imported at exactly one site in the repo, and every call routes throughwrapSync, which sets the correct token (or blanks it for desktop auth) inside the semaphore immediately before invoking. So the onlyopchildren that ever saw a token were the ones whose own call supplied it. op-js also builds a fresh per-spawn env rather than mutatingprocess.env, so no other subprocess inherited it.saveConfigJSON-stringifiesconfig.auth— token included — into the config blob, whichmakeFumaBlobStore.putwrites as a plainvaluecolumn. That is documented intypes.ts.resolve/listre-decodes the stored token into a fresh immutable JS string, and JS strings cannot be zeroed, so unreachable copies linger in the heap regardless.So: this removes a long-lived reachable reference, which is what the library's own API shape implies you should do, and it is cheap and tested. It is not a boundary change.
The larger thing this sits next to
The comment on
ServiceAccountAuth.tokenexplains the plaintext storage as "v1 stored this behind a separate secret id; v2 has no secrets table, so the plugin-owned config row carries it directly."That rationale looks stale: v2 does have a credential-provider abstraction, and a long-lived service-account token is exactly the kind of value it exists for. Moving it there would let a sealed or hardware-backed store hold it instead of a plaintext blob column — a real boundary change, unlike this PR.
I have not written that change, because it is a design decision that is yours to make and it would need a migration for existing configs. Happy to open it if you want it.
Tests
Two tests in
service.test.ts, pinning both halves deliberately: the token must be gone afterwards, and it must still be set while the call runs. Clearing it too early would satisfy a "no longer parked" assertion and silently break authentication.Mutation-checked, each mutation verified to have landed in the file, with an unmutated control before and after:
Package: 10 passed.
tsgo --noEmit,oxlint --deny-warningsandoxfmt --checkclean on the changed files.