security(github-copilot): warn and optionally refuse default token password on shared FS#302
Open
pjdoland wants to merge 2 commits into
Open
security(github-copilot): warn and optionally refuse default token password on shared FS#302pjdoland wants to merge 2 commits into
pjdoland wants to merge 2 commits into
Conversation
mbektas
approved these changes
May 19, 2026
…d on shared FS The default NBI_GH_ACCESS_TOKEN_PASSWORD is a public literal. On a shared-home cluster (NFS-backed JupyterHub, classroom HPC), anyone with read access to another user's ~/.jupyter/nbi/user-data.json can decrypt their Copilot OAuth token. The admin-guide and PRIVACY.md call this out but the code did nothing programmatic to prevent it: no startup warning, no refusal-to-write, no FS-mode probe. Add two opt-in guardrails on the token write path: 1. A per-process WARNING fires on the first read or write of the stored token when the default password is in use. The message names the env var and the documented escape hatches. When the directory that holds user-data.json is readable or writable by group or other (S_IRGRP/S_IROTH/S_IWGRP/S_IWOTH), the warning escalates to call out the directory path so an operator triaging logs can find the file. 2. When NBI_REFUSE_DEFAULT_TOKEN_PASSWORD_ON_SHARED_FS is set, the write is refused entirely under the same conditions: default password AND a group/other-accessible directory. The refusal is opt-in so single-user deployments with incidentally-loose home modes don't break. Admins who knowingly accept the risk during a rollout can opt back in per pod with NBI_ALLOW_DEFAULT_TOKEN_PASSWORD. The shared-dir check is POSIX-only (Windows ACL semantics don't map onto S_IRGRP/S_IROTH); on Windows the gate is a no-op so existing deployments are unaffected. Env-var parsing matches the _resolve_bool_with_env vocab (true/1/yes/on, case-insensitive) so admins don't have to remember a different set of accepted values for this knob. Test coverage in tests/test_default_password_refusal.py covers warn-on both read and write, warn-once-per-process, the escalated message including the directory path, the refusal opt-in matrix (default-off / opt-in shared / opt-in private / opt-in + opt-out / non-default password), and an end-to-end write that returns False with a named error message naming the file. Admin-guide and PRIVACY notes updated to document both new env vars.
Second-pass review surfaced two items: 1. The default-password WARNING mentioned NBI_POD_IDENTITY as a knob the operator could set, but that env var ships in PR plmbr#300 (per-pod KDF) and does not exist on the branch's merge base. Drop the reference until plmbr#300 lands; the remaining knobs (NBI_GH_ACCESS_TOKEN_PASSWORD per-user, NBI_ALLOW_DEFAULT_TOKEN_PASSWORD opt-out) are sufficient guidance for the deployments this PR targets. 2. The read-path warn test only asserted the WARNING fired; the contract is "warn AND return the plaintext" because a regression that aborts the read on warn would silently log the user out. Rewritten to encrypt a real token, run the read, and assert both halves. Also pin that the non-shared branch of the warning omits the directory path so log analysis can tell the two postures apart by message content alone. The silent-fail UX (when NBI_REFUSE_DEFAULT_TOKEN_PASSWORD_ON_SHARED_FS refuses the write, the user sees no in-app signal) is documented as a known limitation in the PR description but not addressed here: plumbing storage-failure status back to the chat sidebar is a separable change that touches the capability handshake, and the audit's recommended remediation (operators set the env vars before rollout) means in-app feedback is a UX enhancement rather than a regression blocker.
709847d to
34d1f53
Compare
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
The default
NBI_GH_ACCESS_TOKEN_PASSWORDis a public literal. On a shared-home cluster, anyone with read access to another user's~/.jupyter/nbi/user-data.jsoncan decrypt their Copilot OAuth token using the documented default.admin-guide.mdandPRIVACY.mdalready call this out, but the code did nothing programmatic to prevent it: no startup warning, no refusal-to-write, no FS-mode probe.Solution
Add two opt-in guardrails on the token write path:
Always-on WARNING when the default password is in use. Fires on the first read or write of the stored token (per process), so an operator sees it in the boot log of any session that touches the file. The message names the env var and the documented escape hatches. When the directory that holds
user-data.jsonis readable or writable by group or other (S_IRGRP/S_IROTH/S_IWGRP/S_IWOTH), the message escalates and names the directory path so a log triage finds the file.Opt-in refusal. Set
NBI_REFUSE_DEFAULT_TOKEN_PASSWORD_ON_SHARED_FS=1and the write is refused entirely when default password AND group/other-accessible directory both hold. The refusal is opt-in so single-user deployments with incidentally-loose home modes (the defaultumask 0o022shape) keep working. Admins who knowingly accept the risk during a rollout, or in single-user contexts where the dir mode is incidental, opt back in per pod withNBI_ALLOW_DEFAULT_TOKEN_PASSWORD=1.The shared-dir check is POSIX-only; on Windows the gate is a no-op because
S_IRGRP/S_IROTHdon't map onto NTFS ACL semantics. Env-var parsing reuses the same vocab as_resolve_bool_with_env(true/1/yes/on, case-insensitive) so admins don't have to remember a different set of accepted values for this knob.The write-bits (
S_IWGRP/S_IWOTH) count too: a writable parent dir lets a neighbor plant a forgeduser-data.jsonso the next session reads the attacker's token, which is the same shape of damage as a readable parent leaking the existing token. The audit specifies "world-readable" but flagging write is cheap defense in depth.Testing
tests/test_default_password_refusal.pycovers:Full suites: pytest 773 pass, tsc clean, prettier clean, jest clean.
Risks / follow-ups
NBI_REFUSE_DEFAULT_TOKEN_PASSWORD_ON_SHARED_FS=1to engage the refusal. The always-on WARNING is new noise for any deployment running with the default password, but only one line per process; a deployment that already sets a per-user password sees no new logs.token_persisted: falsefield on the login-status event; this PR is opt-in (the admin who sets the env understands the tradeoff) so in-app feedback is a UX enhancement rather than a regression blocker.JUPYTERHUB_USERpresence, so cluster deployments get the protection without an extra env. Out of scope here; the opt-in flag is the conservative first step.0o600on theuser-data.jsonfile (not just the directory) is tracked as a separate audit item. With that mode, the directory check becomes belt-and-suspenders; without it the dir check is the primary defense.