Skip to content

onboarding: write ~/.aider/oauth-keys.env at 0o600 instead of default umask (0o644)#5154

Open
JAE0Y2N wants to merge 1 commit into
Aider-AI:mainfrom
JAE0Y2N:harden-oauth-keys-file-perms
Open

onboarding: write ~/.aider/oauth-keys.env at 0o600 instead of default umask (0o644)#5154
JAE0Y2N wants to merge 1 commit into
Aider-AI:mainfrom
JAE0Y2N:harden-oauth-keys-file-perms

Conversation

@JAE0Y2N
Copy link
Copy Markdown

@JAE0Y2N JAE0Y2N commented May 19, 2026

The OpenRouter OAuth flow in aider/onboarding.py (around line 366) saves the user's API key to ~/.aider/oauth-keys.env via plain open(path, "a"). Python's open() honours the process umask, which on standard Linux/macOS installs (umask 022) creates the file at mode 0o644 — world-readable. Any local UID on the same host — another user account, a cron job running as another user, a container sharing the home volume, a sandboxed CI runner mounting the home dir, a co-tenant on a multi-user dev box — can read the OPENROUTER_API_KEY value by walking through the also-default-0o755 ~/.aider/ directory.

The OAuth token granted by OpenRouter is the user's bearer credential against the OpenRouter API. A local reader can quietly use it to make OpenRouter calls under the victim's account (consuming their credit, running prompts under their auditable identity, etc.). This is the same class of issue that gh, aws, gcloud, stripe, docker, and most other vendor CLIs explicitly defend against by writing OAuth/credential files at 0o600 inside a 0o700 directory.

Empirically reproduced before the fix (umask 022, macOS 14):

$ python3 -c '
import os
config_dir = os.path.expanduser("/tmp/aider_poc")
os.makedirs(config_dir, exist_ok=True)
key_file = os.path.join(config_dir, "oauth-keys.env")
with open(key_file, "a", encoding="utf-8") as f:
    f.write("OPENROUTER_API_KEY=\"sk-or-FAKE\"\n")
'
$ ls -ld /tmp/aider_poc /tmp/aider_poc/oauth-keys.env
drwxr-xr-x  3 user staff   96 .../tmp/aider_poc
-rw-r--r--  1 user staff   59 .../tmp/aider_poc/oauth-keys.env

After the fix the same flow produces drwx------ on the directory and -rw------- on the credential file.

Three pieces of hardening land together in this PR:

  1. The directory is explicitly chmoded to 0o700 even if it already exists. os.makedirs(exist_ok=True) does not tighten the mode of an already-existing directory, so a ~/.aider/ previously laid down at 0o755 by another tool (or by a prior aider release) keeps the wider mode without an explicit tightening step.

  2. When the credential file does not yet exist, it is created via os.open(... O_WRONLY|O_CREAT|O_TRUNC, 0o600) and then wrapped in os.fdopen for the actual write. This avoids the TOCTOU window where the file would otherwise be briefly visible at the default 0o644 mode between the create-at-default and a follow-up chmod.

  3. When the credential file already exists (a re-run of the OAuth flow against the same install), the append semantics are preserved but an explicit os.chmod(key_file, 0o600) follows the write so the mode is corrected even on the re-run path.

Windows / non-Unix platforms swallow the chmod errors via except (OSError, NotImplementedError), so the behaviour matches the prior code on those platforms while gaining hardening on Unix-like hosts.

This is the same class of finding I've reported privately against several other vendor CLIs that store credentials with the default Python open() mode — most recently against huggingface/huggingface_hub (PR #4234, approved by @Wauplin with a refactor request that landed the _write_secret helper using os.open(O_CREAT, 0o600)), eosphoros-ai/DB-GPT (#3077, same pattern), and several Node-ecosystem CLIs (cline GHSA-58hj-8g5g-q689, replicate GHSA-9g42-hx86-3ppm, prisma GHSA-cgq2-43wx-qgpq, cloudflare workers-sdk GHSA-hfj5-88mp-26jq today). The aider instance is structurally identical, just in Python with the OpenRouter token specifically.

I filed this as a PR rather than a private vulnerability report because the repo doesn't have Private Vulnerability Reporting enabled and no SECURITY.md / direct security contact. If you'd prefer a private channel for future reports, enabling Settings → Security → Private vulnerability reporting adds a private intake on this repo without any other process change. Happy to coordinate timeline / hold a public advisory until after the fix releases if that's preferable to a PR-as-disclosure flow.

— Jaeyoung Yun (JAE0Y2N)

… umask (0o644)

The OpenRouter OAuth flow saves the user's API key to
~/.aider/oauth-keys.env via plain `open(path, "a")`. Python's open()
honours the process umask, which on standard Linux and macOS installs
(umask 022) creates the file at mode 0o644 — world-readable. Any
local UID on the same machine (other user accounts, cron jobs running
as another user, container sharing the home volume, sandboxed CI
runner mounting the home dir, co-tenant on a multi-user dev box) can
read the OPENROUTER_API_KEY value from the file by walking through
the also-default-0o755 ~/.aider/ directory.

This brings the credential file in line with the cross-CLI baseline
that gh, aws, gcloud, stripe, docker, and most other vendor CLIs
follow for OAuth-bearing files: 0o600 file + 0o700 directory.

Three pieces of hardening land together:

1. The directory is explicitly chmoded to 0o700 even if it already
   exists, since `os.makedirs(exist_ok=True)` does not tighten an
   already-existing directory's mode.

2. When the credential file does not yet exist, it is created via
   `os.open(... O_WRONLY|O_CREAT|O_TRUNC, 0o600)` so the file is
   never visible at a wider mode at any point in its lifetime
   (avoids TOCTOU between create-at-default-mode and a follow-up
   chmod).

3. When the credential file already exists (re-running the OAuth
   flow against the same install), the existing-file path keeps the
   append semantics but follows up with an explicit
   os.chmod(key_file, 0o600) so the mode is corrected on every save.

Empirically verified — the new code produces -rw------- on macOS and
Linux umask 022 hosts; the prior code produced -rw-r--r-- on the
same hosts.

The fix is conservative — Windows / non-Unix platforms swallow the
chmod errors via the (OSError, NotImplementedError) except, so the
behaviour matches the prior code on those platforms while gaining
hardening on Unix-like systems.
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@JAE0Y2N
Copy link
Copy Markdown
Author

JAE0Y2N commented May 20, 2026

I have read the CLA Document and I hereby sign the CLA

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants