mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 22:19:42 +02:00
Widening the servable extensions to EDITABLE_EXTENSIONS made ~/.codeman JSON previewable for the first time, and the blocklist named only state.json. But settings.json holds a credential BY SCHEMA (voiceSettings.apiKey), push-keys.json holds the VAPID PRIVATE key, and intents.json is written 0600 precisely because captured prompts can carry secrets — all three were one authenticated click away once an agent printed the path. Blocked alongside state.json, whose rule now also catches state-* siblings. The never-re-cuts-inside-an-anchor test used an unmatchable URL tail, so it passed with the guard deleted; the fixture now carries a matchable /tmp path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
109 lines
4.6 KiB
TypeScript
109 lines
4.6 KiB
TypeScript
/**
|
|
* @fileoverview Shared sensitive-path blocklist.
|
|
*
|
|
* A small defense-in-depth blocklist of absolute paths that must never be
|
|
* served to the browser regardless of how the path was obtained (workspace
|
|
* download, cross-workspace attachment registration, raw/preview serving).
|
|
*
|
|
* This is intentionally a BLOCKLIST, not a workspace-confinement check:
|
|
* cross-workspace attachment is a supported feature (codeman-publish skill +
|
|
* the automated review-card loop attaching files under ~/.codeman/), so a
|
|
* strict session-workspace boundary would break legitimate use. The blocklist
|
|
* rejects well-known secret locations (system password files, SSH keys, cloud
|
|
* credentials, dotenv files) while leaving ordinary cross-workspace files
|
|
* attachable.
|
|
*
|
|
* ⚠️ The path picker's `showHidden` option is what makes the dot-prefixed half
|
|
* of this list load-bearing. Before it existed, the picker refused every path
|
|
* with a hidden segment, so `~/.config/gh/hosts.yml` and friends were
|
|
* unreachable by construction and the list only had to cover the few secrets
|
|
* that live in plain sight. Opting into hidden entries removes that accident,
|
|
* so every credential location below has to be named. Adding a new browse
|
|
* surface means re-reading this file, not assuming it already covers you.
|
|
*
|
|
* ⚠️ Deliberately NOT whole-tree blocks: `~/.codeman/` (the publish skill
|
|
* attaches from it) and `~/.claude/` (transcripts and team state are ordinary
|
|
* files worth attaching). Only their secret-bearing members are named.
|
|
*
|
|
* Callers MUST resolve symlinks (realpath) BEFORE calling isSensitivePath so a
|
|
* symlink pointing at a sensitive target is also caught.
|
|
*/
|
|
|
|
const SENSITIVE_PATTERNS: RegExp[] = [
|
|
// System account databases.
|
|
/^\/etc\/shadow$/,
|
|
/^\/etc\/gshadow$/,
|
|
/^\/etc\/master\.passwd$/,
|
|
|
|
// SSH and GPG private key material. `.ssh/` is matched at any depth rather
|
|
// than only under homedir(): a per-project or per-deploy key directory holds
|
|
// exactly the same secret, and it drops a homedir() read that is captured at
|
|
// module load and therefore wrong for anything that changes HOME later.
|
|
/\/\.ssh\//,
|
|
/\/\.gnupg\//,
|
|
|
|
// Dotenv, in every conventional spelling (.env, .env.local, .env.production).
|
|
/\/\.env$/,
|
|
/\/\.env\./,
|
|
|
|
// Generic credential files, plus the per-vendor spellings that do not match it.
|
|
/\/credentials(\.json|\.yml|\.yaml|\.xml|\.toml|\.db)?$/i,
|
|
/\/\.aws\/(credentials|config)$/,
|
|
/\/\.aws\/sso\/cache\//,
|
|
/\/\.gcloud\/credentials\.db$/,
|
|
/\/\.config\/gcloud\//,
|
|
/\/\.azure\//,
|
|
/\/\.docker\/config\.json$/,
|
|
/\/\.kube\/config$/,
|
|
|
|
// Package-registry and forge tokens. Each of these is a bearer credential in
|
|
// a plain-text dotfile, which is exactly what a path picker will surface.
|
|
/\/\.npmrc$/,
|
|
/\/\.yarnrc\.yml$/,
|
|
/\/\.git-credentials$/,
|
|
/\/\.config\/gh\//,
|
|
/\/\.config\/hub$/,
|
|
/\/\.netrc$/,
|
|
/\/_netrc$/,
|
|
/\/\.pypirc$/,
|
|
/\/\.gem\/credentials$/,
|
|
/\/\.cargo\/credentials(\.toml)?$/,
|
|
/\/\.terraformrc$/,
|
|
/\/\.terraform\.d\//,
|
|
|
|
// Database client credentials.
|
|
/\/\.pgpass$/,
|
|
/\/\.my\.cnf$/,
|
|
|
|
// Agent CLI credentials, including Codeman's own hook secret and user table.
|
|
// Named individually so the surrounding trees stay attachable (see above).
|
|
/\/\.claude\/\.credentials\.json$/,
|
|
/\/\.codeman[^/]*\/hook-secret$/,
|
|
/\/\.codeman[^/]*\/users\.json$/,
|
|
// Codeman's own state files. Named once `.json` became previewable outside
|
|
// the workspace: `SessionState.envOverrides` persists whatever the user set
|
|
// for a session, and the env allowlist admits key-shaped names
|
|
// (`GEMINI_API_KEY`, `CLAUDE_CODE_*`), so state can hold a live credential.
|
|
// `state[^/]*` rather than `state`: siblings like state-inner.json carry the
|
|
// same payload. Same reasoning as the two entries above, and it leaves the
|
|
// rest of ~/.codeman attachable.
|
|
/\/\.codeman[^/]*\/state[^/]*\.json$/,
|
|
// settings.json holds a credential BY SCHEMA (`voiceSettings.apiKey`, the
|
|
// Deepgram key); push-keys.json holds the VAPID PRIVATE key (enough to forge
|
|
// push notifications to every subscribed device); intents.json is written
|
|
// 0600 precisely because captured prompts can contain secrets, and is
|
|
// deliberately kept out of /api/search — it must not be readable through a
|
|
// different route instead.
|
|
/\/\.codeman[^/]*\/settings\.json$/,
|
|
/\/\.codeman[^/]*\/push-keys\.json$/,
|
|
/\/\.codeman[^/]*\/intents\.json$/,
|
|
];
|
|
|
|
/**
|
|
* Returns true if the given ABSOLUTE, symlink-resolved path matches the
|
|
* sensitive-file blocklist and must not be served to the browser.
|
|
*/
|
|
export function isSensitivePath(absPath: string): boolean {
|
|
return SENSITIVE_PATTERNS.some((pattern) => pattern.test(absPath));
|
|
}
|