Testing 1Password
In October of 2023, I reported a vulnerability to 1Password regarding their op (a.k.a. 1password-cli ) program. In my report I detailed that their approach to prompting users only once, and then leaving the vault open to the CLI was easily exploited in supply-chain scenarios, especially when a threat actor targets developer toolchains. There are two attack paths I highlighted, and I supplied them with a proof for one of them.
Warning This document is for research and educational purposes. Any use for the information below to cause harm or engaged in unauthorized access of any computer system is strictly prohibited. Responsible disclosure was given on 2nd October, 2023 to 1Password, and in January of 2024 1Password authorized public disclosure of this vulnerability via BugCrowd.
This demo was tested across the three most recent versions of macOS, using zsh and bash shells using the latest 1Password desktop client.
Two Attack Paths
Both attacks would be a supply-chain attack, but there are two distinct paths:
IDE Path
The IDE path is pretty straight-forward, and I think carries the greatest risk:
I install the 1Password extension because I responsibly wish to keep my tokens in a safe place (e.g. not my $ENV ) I also use the MySQL extension in my IDE, it's nice to be able to stay in the same tool I use the 1Password extension to resolve secret references, which requires me to unlock my vault I installed a new red theme, red is my favorite color That red theme is an extension, and contained malicious code which uses the op NPM module to enumerate and exfiltrate every vault that I have access to
Package manager path
I install the 1Password CLI, and I use op to protect secrets in my environment I use GitHub Packages for NPM packages which are private to my organization I hear of a really nifty plugin which will allow me to add syntax highlighting to shell output on this CLI project I'm working on, so I npm i syntax-highlighting-stuff Oh no! syntax-highlighting-stuff had a post-install script on it, and it enumerated and exfiltrated the secrets from every vault I have access to
Observed patterns
It seems like the vulnerability is that once you unlock your vault, anything spawned from the parent process of whatever opened the vault retains an active session to that open vault.
$ op run -- ls # This prompts me to unlock my vault $ op run -- ls # The second call does not prompt me, the vault is already open $ op read 'op://Foo/Bar/baz' # Still doesn't prompt me again because the vault is still open
This also works with subprocesses:
$ export GITHUB_TOKEN = 'op://Foo/Bar/baz' $ op run -- env | grep GITHUB_TOKEN # This will prompt me $ bash # Start a new shell subprocess $ op run -- env | grep GITHUB_TOKEN # This will not prompt me $ bash # Now we're two shells deep in subprocesses $ op run -- env | grep GITHUB_TOKEN # This will still not prompt me
The Proof
This repository contains the code from the proof that I submitted to 1Password on 2nd October, 2023. Here are the instructions for running the proof:
The index.cjs has a module which runs the naughty module.
has a module which runs the module. Either using netcat or simple-exfil-service , listen on port 4242
, listen on port To run this test, simply run op run npm install like you needed a GitHub token
like you needed a GitHub token Afterward, check your output from port 4242, but also check to see if there is a /tmp/naughty file
Here's what the person running npm i would see:
❯ op run -- npm i > [email protected] postinstall > node ./index.cjs theItem: { "id" : "[redacted]" , "title" : "Fake Website Login" , "version" : 1, "vault" : { "id" : "[redacted]" , "name" : "Employee" } , "category" : "LOGIN" , "last_edited_by" : "[redacted]" , "created_at" : "2023-10-02T17:28:50Z" , "updated_at" : "2023-10-02T17:28:50Z" , "additional_information" : "fake.user" , "fields" : [ { "id" : "username" , "type" : "STRING" , "purpose" : "USERNAME" , "label" : "username" , "value" : "fake.user" , "reference" : "op://Employee/Fake Website Login/username" } , { "id" : "password" , "type" : "CONCEALED" , "purpose" : "PASSWORD" , "label" : "password" , "value" : "this-is-the-fake-password-in-plaintext" , "reference" : "op://Employee/Fake Website Login/password" , "password_details" : { "strength" : "FANTASTIC" } } , { "id" : "notesPlain" , "type" : "STRING" , "purpose" : "NOTES" , "label" : "notesPlain" , "reference" : "op://Employee/Fake Website Login/notesPlain" } ] } Done. up to date, audited 8 packages in 5s found 0 vulnerabilities
You can see that the demo attack is printing those values to STDOUT. I am only dumping one value, but the op program and JavaScript library do have the ability to enumerate items in a vault, and vaults themselves.
Here's what my exfiltration server sees:
Request Headers: { host: 'localhost:4242', connection: 'keep-alive', 'content-type': 'text/plain;charset=UTF-8', accept: '*/*', 'accept-language': '*', 'sec-fetch-mode': 'cors', 'user-agent': 'node', 'accept-encoding': 'gzip, deflate', 'content-length': '1082' } Request URL: / Received data: { "id": "[redacted]", "title": "Fake Website Login", "version": 1, "vault": { "id": "[redacted]", "name": "Employee" }, "category": "LOGIN", "last_edited_by": "[redacted]", "created_at": "2023-10-02T17:28:50Z", "updated_at": "2023-10-02T17:28:50Z", "additional_information": "fake.user", "fields": [ { "id": "username", "type": "STRING", "purpose": "USERNAME", "label": "username", "value": "fake.user", "reference": "op://Employee/Fake Website Login/username" }, { "id": "password", "type": "CONCEALED", "purpose": "PASSWORD", "label": "password", "value": "this-is-the-fake-password-in-plaintext", "reference": "op://Employee/Fake Website Login/password", "password_details": { "strength": "FANTASTIC" } }, { "id": "notesPlain", "type": "STRING", "purpose": "NOTES", "label": "notesPlain", "reference": "op://Employee/Fake Website Login/notesPlain" } ] }
Notice that the value for the password is in plaintext in both cases.
The Risk
The 1Password CLI is marketed as a tool which makes technical practitioners safer by protecting credentials that are traditionally stored in plaintext in a user's environment variables on their local machine. This vulnerability demonstrates that while this does get the secrets out of your environment, it also drastically expands the potential blast radius for a successful malware or supply-chain attack.
To put it simply: the risk here is not that your GitHub secret will be leaked via an environment variable, the risk is that every vault you have access to could be dumped by a threat actor.
Additionally, as agentic AI tools become more commonplace, that may add additional risk factors which have yet to be considered in the research I'm presenting here.
The op tool doesn't just possess the ability to get individual items, it also has the ability to enumerate your vaults ( op vault list ) and to enumerate items in a given vault ( op item list --vault abc123 ). The JavaScript module supports all of the same commands that the op CLI tool does, too.
Attempts to Mitigate
I have explored a number of paths to mitigate this.
Following the suggestion of a colleague, I experimented with using a separate vault for CLI secrets This doesn't work because you cannot limit the default vault from being read by the CLI Not only that, but you can't set limits for things like shared vaults As weird as it sounds, when you unlock one vault, you unlock all vaults which are accessible to the CLI tool
1Password recommended using service accounts to mitigate this, and I did try it, but I found some challenges as I started thinking of how to roll it out to teams This kinda sucks because that means each developer gets a separate service account user on their workstation This also means that the developer has to manage a service account In addition to being unweildy, this also means that each engineer must be diligent to not enable the shiny "Integrate with 1Password CLI" button in the Developer tab on the GUI settings
Recommendations
I recommend that folks avoid using op on developer workstations until 1Password has released a fix for these scenarios The best way to do this appears to be to make sure CLI integration checkbox is unchecked in the Developer settings screen.
on developer workstations until 1Password has released a fix for these scenarios I recommend that when you must use op , that it be limited to service accounts, per 1Password's recommendation, and that you carefully verify that the "Integrate with 1Password CLI" box is unchecked in the GUI settings
, that it be limited to service accounts, per 1Password's recommendation, and that you carefully verify that the "Integrate with 1Password CLI" box is unchecked in the GUI settings I recomment that, where possible, you get in the habit of always passing --ignore-scripts to npm commands, and find a similar pattern for any other package manager that you use in conjunction with op
I strongly recommend that 1Password modify their product to resolve this problem. Just spitballing, I think any of the following would be sufficient (this is an OR, not an AND):
Allow users to limit access to vaults using CLI integrations
Allow users to designate individual items in their Vaults for use with the CLI
Prompt for specific vaults or items individually
Prompt for each process individually, closing the gap for subprocesses
Conclusion
This investigation took a while, and I waited a while before publishing this disclosure (life circumstances and giving 1Password time to fix the issue). While 1Password is within their right not to issue a CVE or a fix for this vulnerability, I do think 1Password users (I am proud to be one) would be much safer if this issue were eliminated.
Thanks, please contact me with any corrections or feedback.