singularity-forge/src/security-overrides.ts
Justin Wyer 71caa18552 fix(security): add configurable overrides for command allowlist and SSRF blocklist
PR #666 introduced hardcoded SAFE_COMMAND_PREFIXES and SSRF URL
blocklists with no override mechanism. Users with non-standard
credential tools (sops, doppler, age, infisical) or needing to fetch
from internal URLs (self-hosted docs, VPN services) were silently
blocked with no recourse.

Add two global-only settings (ignored in project-level settings.json
to preserve the security property against malicious repos):

- allowedCommandPrefixes: replaces the built-in command allowlist
- fetchAllowedUrls: exempts hostnames from SSRF blocking

Both also support env var overrides (GSD_ALLOWED_COMMAND_PREFIXES,
GSD_FETCH_ALLOWED_URLS) for CI/container environments. Env vars
take precedence over settings.json.

Security model: global-only keys are stripped from project settings
at load time via stripGlobalOnlyKeys(), applied at all three
assignment points for this.projectSettings. The merge function
stays untouched — no future caller can accidentally skip stripping.

15 new tests covering override behavior, cache invalidation,
allowlist exemptions, and global-only enforcement.
2026-04-02 13:45:05 +02:00

42 lines
1.5 KiB
TypeScript

/**
* Apply user-configured security overrides from global settings.json and env vars.
*
* Both overrides are global-only (not project-level) because the threat model is
* malicious project-level config in cloned repos. Global settings and env vars
* represent the user's own authority on their machine.
*
* Precedence: env var > settings.json > built-in defaults
*/
import { type SettingsManager, setAllowedCommandPrefixes } from '@gsd/pi-coding-agent'
import { setFetchAllowedUrls } from './resources/extensions/search-the-web/url-utils.js'
export function applySecurityOverrides(settingsManager: SettingsManager): void {
// --- Command prefix allowlist ---
const envPrefixes = process.env.GSD_ALLOWED_COMMAND_PREFIXES
if (envPrefixes) {
const prefixes = envPrefixes.split(',').map(s => s.trim()).filter(Boolean)
if (prefixes.length > 0) {
setAllowedCommandPrefixes(prefixes)
}
} else {
const settingsPrefixes = settingsManager.getAllowedCommandPrefixes()
if (settingsPrefixes && settingsPrefixes.length > 0) {
setAllowedCommandPrefixes(settingsPrefixes)
}
}
// --- Fetch URL allowlist (SSRF exemptions) ---
const envUrls = process.env.GSD_FETCH_ALLOWED_URLS
if (envUrls) {
const urls = envUrls.split(',').map(s => s.trim()).filter(Boolean)
if (urls.length > 0) {
setFetchAllowedUrls(urls)
}
} else {
const settingsUrls = settingsManager.getFetchAllowedUrls()
if (settingsUrls && settingsUrls.length > 0) {
setFetchAllowedUrls(settingsUrls)
}
}
}