mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(security): close remaining CodeQL alerts + document mandatory patterns
Fixes the 4 fixable alerts opened in the recent scan and adds enforceable guardrails so future development follows the same pattern. Code fixes: - src/mitm/cert/install.ts: pass certPath/certName/action via exec()'s env option instead of string-interpolating them into the bash script (CodeQL js/shell-command-injection-from-environment #225) - scripts/docs/{gen-provider-reference,add-frontmatter,fix-internal-links}: escape backslash before other regex/markdown metacharacters (CodeQL js/incomplete-sanitization #227, #228, #229) Documentation (mandatory patterns): - docs/security/PUBLIC_CREDS.md — embedding public upstream OAuth/Firebase identifiers via resolvePublicCred(); never as string literals - docs/security/ERROR_SANITIZATION.md — routing every error response through sanitizeErrorMessage()/buildErrorBody(); never raw err.stack/err.message - CLAUDE.md: 4 new Hard Rules (#11-#14) + Security section + scenario notes - AGENTS.md, CONTRIBUTING.md: cross-reference the two new docs - SECURITY.md: extended Hard Security Rules with the new mandatory patterns - docs/README.md: index entries pointing to the two new docs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -120,6 +120,10 @@ Always run `prettier --write` on changed files.
|
||||
- Auth middleware required on all API routes
|
||||
- Never log SQLite encryption keys
|
||||
- Sanitize user content (dompurify for HTML)
|
||||
- **Public upstream OAuth identifiers** (Gemini / Antigravity / Windsurf-style client_id/secret + Firebase Web keys extracted from public CLIs): use `resolvePublicCred()` from `open-sse/utils/publicCreds.ts`, **never** as string literals. Full pattern in `docs/security/PUBLIC_CREDS.md`.
|
||||
- **Error responses** (HTTP / SSE / executor / MCP): use `buildErrorBody()` or `sanitizeErrorMessage()` from `open-sse/utils/error.ts`, **never** put raw `err.stack` / `err.message` in a Response body. Full pattern in `docs/security/ERROR_SANITIZATION.md`.
|
||||
- **`exec()` / `spawn()` with runtime values**: pass via the `env` option, **never** string-interpolate paths/values into the script body. Reference: `src/mitm/cert/install.ts::updateNssDatabases`.
|
||||
- Prefer secure-by-default libraries when available — see [tldrsec/awesome-secure-defaults](https://github.com/tldrsec/awesome-secure-defaults) for the curated list (Helmet.js, DOMPurify, ssrf-req-filter, safe-regex, Google Tink, etc.).
|
||||
|
||||
---
|
||||
|
||||
|
||||
17
CLAUDE.md
17
CLAUDE.md
@@ -244,6 +244,10 @@ connection continue serving other models.
|
||||
- Validate all inputs with Zod schemas
|
||||
- Encrypt credentials at rest (AES-256-GCM)
|
||||
- Upstream header denylist: `src/shared/constants/upstreamHeaders.ts` — keep sanitize, Zod schemas, and unit tests aligned when editing
|
||||
- **Public upstream credentials** (Gemini/Antigravity/Windsurf-style OAuth client_id/secret + Firebase Web keys extracted from public CLIs): **MUST** be embedded via `resolvePublicCred()` from `open-sse/utils/publicCreds.ts` — **never** as string literals. See `docs/security/PUBLIC_CREDS.md` for the mandatory pattern.
|
||||
- **Error responses** (HTTP / SSE / executor / MCP handler): **MUST** route through `buildErrorBody()` or `sanitizeErrorMessage()` from `open-sse/utils/error.ts` — **never** put raw `err.stack` or `err.message` in a response body. See `docs/security/ERROR_SANITIZATION.md`.
|
||||
- **Shell commands built from variables**: when calling `exec()`/`spawn()` with a script that needs runtime values, pass them via the `env` option (shell-escaped automatically) — **never** string-interpolate untrusted/external paths into the script body. Reference: `src/mitm/cert/install.ts::updateNssDatabases`.
|
||||
- **Secure-by-default libraries** ([tldrsec/awesome-secure-defaults](https://github.com/tldrsec/awesome-secure-defaults)): prefer Helmet.js, DOMPurify, ssrf-req-filter, safe-regex, Google Tink over custom implementations whenever adding new security-sensitive surfaces.
|
||||
|
||||
---
|
||||
|
||||
@@ -254,9 +258,9 @@ connection continue serving other models.
|
||||
1. Register in `src/shared/constants/providers.ts` (Zod-validated at load)
|
||||
2. Add executor in `open-sse/executors/` if custom logic needed (extend `BaseExecutor`)
|
||||
3. Add translator in `open-sse/translator/` if non-OpenAI format
|
||||
4. Add OAuth config in `src/lib/oauth/constants/oauth.ts` if OAuth-based
|
||||
4. Add OAuth config in `src/lib/oauth/constants/oauth.ts` if OAuth-based — if the upstream CLI ships a public client_id/secret, embed via `resolvePublicCred()` (see `docs/security/PUBLIC_CREDS.md`), **never** as a literal
|
||||
5. Register models in `open-sse/config/providerRegistry.ts`
|
||||
6. Write tests in `tests/unit/`
|
||||
6. Write tests in `tests/unit/` (include the publicCreds shape assertion if you added a new embedded default)
|
||||
|
||||
### Adding a New API Route
|
||||
|
||||
@@ -264,7 +268,8 @@ connection continue serving other models.
|
||||
2. Create `route.ts` with `GET`/`POST` handlers
|
||||
3. Follow pattern: CORS → Zod body validation → optional auth → handler delegation
|
||||
4. Handler goes in `open-sse/handlers/` (import from there, not inline)
|
||||
5. Add tests
|
||||
5. Error responses use `buildErrorBody()` / `errorResponse()` from `open-sse/utils/error.ts` (auto-sanitized — never put `err.stack` or `err.message` raw in the body). See `docs/security/ERROR_SANITIZATION.md`.
|
||||
6. Add tests — including at least one assertion that error responses do not leak stack traces (`!body.error.message.includes("at /")`)
|
||||
|
||||
### Adding a New DB Module
|
||||
|
||||
@@ -323,6 +328,8 @@ For any non-trivial change, read the matching deep-dive first:
|
||||
| Memory system (FTS5 + Qdrant) | `docs/frameworks/MEMORY.md` |
|
||||
| Cloud agents | `docs/frameworks/CLOUD_AGENT.md` |
|
||||
| Guardrails (PII / injection / vision) | `docs/security/GUARDRAILS.md` |
|
||||
| Public upstream credentials (Gemini/etc.) | `docs/security/PUBLIC_CREDS.md` |
|
||||
| Error message sanitization | `docs/security/ERROR_SANITIZATION.md` |
|
||||
| Evals | `docs/frameworks/EVALS.md` |
|
||||
| Compliance / audit | `docs/security/COMPLIANCE.md` |
|
||||
| Webhooks | `docs/frameworks/WEBHOOKS.md` |
|
||||
@@ -402,3 +409,7 @@ git push -u origin feat/your-feature
|
||||
8. Always include tests when changing production code
|
||||
9. Coverage must stay ≥75% (statements, lines, functions) / ≥70% (branches). Current measured: ~82%.
|
||||
10. Never bypass Husky hooks (`--no-verify`, `--no-gpg-sign`) without explicit operator approval.
|
||||
11. Never embed public upstream OAuth client_id/secret or Firebase Web keys as string literals — always go through `resolvePublicCred()` (`open-sse/utils/publicCreds.ts`). See `docs/security/PUBLIC_CREDS.md`.
|
||||
12. Never return raw `err.stack` / `err.message` in HTTP / SSE / executor responses — always route through `buildErrorBody()` or `sanitizeErrorMessage()` (`open-sse/utils/error.ts`). See `docs/security/ERROR_SANITIZATION.md`.
|
||||
13. Never string-interpolate external paths or runtime values into shell scripts passed to `exec()`/`spawn()` — pass via the `env` option instead. Reference: `src/mitm/cert/install.ts::updateNssDatabases`.
|
||||
14. Never dismiss a CodeQL / Secret-Scanning alert without (a) first checking the pattern docs above to see if the helper applies, and (b) recording the technical justification in the dismissal comment.
|
||||
|
||||
@@ -266,6 +266,10 @@ Create request/response translators in `open-sse/translator/`.
|
||||
|
||||
Add OAuth credentials in `src/lib/oauth/constants/oauth.ts` and service in `src/lib/oauth/services/`.
|
||||
|
||||
If the upstream provider distributes a public OAuth client_id/secret or Firebase Web API key inside its public CLI / browser bundle, **do not** embed it as a string literal. Use `resolvePublicCred()` from `open-sse/utils/publicCreds.ts` and add a masked byte entry to `EMBEDDED_DEFAULTS`. The full mandatory workflow is documented in [`docs/security/PUBLIC_CREDS.md`](./docs/security/PUBLIC_CREDS.md).
|
||||
|
||||
Inside handlers/executors, error messages reaching the client must go through `buildErrorBody()` / `sanitizeErrorMessage()` from `open-sse/utils/error.ts` — never put raw `err.stack` or `err.message` in a Response body. See [`docs/security/ERROR_SANITIZATION.md`](./docs/security/ERROR_SANITIZATION.md).
|
||||
|
||||
### Step 5: Register Models
|
||||
|
||||
Add model definitions in `open-sse/config/providerRegistry.ts`.
|
||||
@@ -287,9 +291,13 @@ Write unit tests in `tests/unit/` covering at minimum:
|
||||
- [ ] Build succeeds (`npm run build`)
|
||||
- [ ] TypeScript types added for new public functions and interfaces
|
||||
- [ ] No hardcoded secrets or fallback values
|
||||
- [ ] Public upstream credentials embedded via `resolvePublicCred()` (see [`docs/security/PUBLIC_CREDS.md`](./docs/security/PUBLIC_CREDS.md)), never as literals
|
||||
- [ ] Error responses route through `buildErrorBody()` / `sanitizeErrorMessage()` — no raw stack traces in response bodies (see [`docs/security/ERROR_SANITIZATION.md`](./docs/security/ERROR_SANITIZATION.md))
|
||||
- [ ] Shell commands (`exec` / `spawn`) pass runtime values via `env`, not via string interpolation
|
||||
- [ ] All inputs validated with Zod schemas
|
||||
- [ ] CHANGELOG updated (if user-facing change)
|
||||
- [ ] Documentation updated (if applicable)
|
||||
- [ ] No new CodeQL / Secret-Scanning alerts opened, or each one dismissed with technical justification referencing the relevant `docs/security/` doc
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -194,19 +194,26 @@ docker run -d \
|
||||
|
||||
These rules are enforced by tooling and reviewers:
|
||||
|
||||
1. **Never commit secrets** — `.env` is gitignored; `.env.example` is the template
|
||||
1. **Never commit secrets** — `.env` is gitignored; `.env.example` is the template (no literals, comments only — see PUBLIC_CREDS.md below)
|
||||
2. **Never use `eval()`, `new Function()`, or implied eval** — ESLint enforces
|
||||
3. **Never bypass Husky hooks** (`--no-verify`, `--no-gpg-sign`) without explicit operator approval
|
||||
4. **Never write raw SQL in routes** — always go through `src/lib/db/` (parameterized)
|
||||
5. **Always validate inputs with Zod** — `src/shared/validation/schemas.ts`
|
||||
6. **Always sanitize upstream headers** — denylist in `src/shared/constants/upstreamHeaders.ts`
|
||||
7. **Encrypt credentials at rest** — AES-256-GCM via `src/lib/db/encryption.ts`
|
||||
8. **Public upstream OAuth identifiers via `resolvePublicCred()`** — never embed `AIza…` / `GOCSPX-…` / `…apps.googleusercontent.com` literals in source. See [`docs/security/PUBLIC_CREDS.md`](docs/security/PUBLIC_CREDS.md).
|
||||
9. **Error responses through `buildErrorBody()` / `sanitizeErrorMessage()`** — never put raw `err.stack` / `err.message` in HTTP / SSE / executor / MCP response bodies. See [`docs/security/ERROR_SANITIZATION.md`](docs/security/ERROR_SANITIZATION.md).
|
||||
10. **`exec()` / `spawn()` runtime values via the `env` option** — never string-interpolate external paths or untrusted values into shell-passed scripts. Reference: `src/mitm/cert/install.ts::updateNssDatabases`.
|
||||
11. **Prefer secure-by-default libraries** — see [tldrsec/awesome-secure-defaults](https://github.com/tldrsec/awesome-secure-defaults) (Helmet.js, DOMPurify, ssrf-req-filter, safe-regex, Google Tink). Reach for them before rolling your own.
|
||||
|
||||
## References
|
||||
|
||||
- [`docs/architecture/AUTHZ_GUIDE.md`](docs/architecture/AUTHZ_GUIDE.md) — authorization pipeline
|
||||
- [`docs/security/GUARDRAILS.md`](docs/security/GUARDRAILS.md) — guardrails framework
|
||||
- [`docs/security/COMPLIANCE.md`](docs/security/COMPLIANCE.md) — audit log and retention
|
||||
- [`docs/security/PUBLIC_CREDS.md`](docs/security/PUBLIC_CREDS.md) — **mandatory** pattern for public upstream credentials
|
||||
- [`docs/security/ERROR_SANITIZATION.md`](docs/security/ERROR_SANITIZATION.md) — **mandatory** pattern for error responses
|
||||
- [`docs/architecture/RESILIENCE_GUIDE.md`](docs/architecture/RESILIENCE_GUIDE.md) — circuit breaker + cooldown + lockout
|
||||
- [`docs/security/STEALTH_GUIDE.md`](docs/security/STEALTH_GUIDE.md) — TLS fingerprinting (legal/ethical notice)
|
||||
- [`CLAUDE.md`](CLAUDE.md) — hard rules for AI agents
|
||||
- [tldrsec/awesome-secure-defaults](https://github.com/tldrsec/awesome-secure-defaults) — curated secure-by-default libraries
|
||||
|
||||
@@ -70,11 +70,13 @@ Combo routing, scoring, and replay.
|
||||
|
||||
## security/
|
||||
|
||||
Guardrails, compliance, and stealth.
|
||||
Guardrails, compliance, stealth, and the mandatory patterns for handling public credentials and error messages.
|
||||
|
||||
- [GUARDRAILS.md](security/GUARDRAILS.md) — PII, prompt injection, vision guardrails.
|
||||
- [COMPLIANCE.md](security/COMPLIANCE.md) — audit trails and compliance.
|
||||
- [STEALTH_GUIDE.md](security/STEALTH_GUIDE.md) — TLS / fingerprint stealth.
|
||||
- [PUBLIC_CREDS.md](security/PUBLIC_CREDS.md) — **mandatory** pattern for embedding public upstream OAuth client_id/secret + Firebase Web keys without tripping secret scanners.
|
||||
- [ERROR_SANITIZATION.md](security/ERROR_SANITIZATION.md) — **mandatory** pattern for routing every error response through `sanitizeErrorMessage` to prevent stack-trace exposure.
|
||||
|
||||
## compression/
|
||||
|
||||
|
||||
140
docs/security/ERROR_SANITIZATION.md
Normal file
140
docs/security/ERROR_SANITIZATION.md
Normal file
@@ -0,0 +1,140 @@
|
||||
---
|
||||
title: "Error Message Sanitization"
|
||||
version: 3.8.0
|
||||
lastUpdated: 2026-05-14
|
||||
---
|
||||
|
||||
# Error Message Sanitization
|
||||
|
||||
> **Source of truth:** `open-sse/utils/error.ts` — `sanitizeErrorMessage`, `buildErrorBody`, `createErrorResult`
|
||||
> **Tests:** `tests/unit/error-message-sanitization.test.ts`
|
||||
> **Last updated:** 2026-05-14 — v3.8.0
|
||||
> **Audience:** Any engineer touching error responses (HTTP routes, SSE streams, executors, MCP handlers).
|
||||
> **Status:** **MANDATORY** for every code path that returns an error message to a client.
|
||||
|
||||
## Why this exists
|
||||
|
||||
CodeQL rule `js/stack-trace-exposure` (CWE-209) flags any code path where an error message originating from a runtime exception reaches an HTTP / SSE response without being sanitized. Stack traces and absolute file paths in production responses give attackers:
|
||||
|
||||
- Internal directory layout (`/srv/app/src/lib/...`) → reconnaissance for further attacks.
|
||||
- Library / framework versions inferred from stack frames → targeted exploit selection.
|
||||
- Sensitive runtime values that may be string-interpolated into errors (DB queries, config values).
|
||||
|
||||
The `sanitizeErrorMessage` helper in `open-sse/utils/error.ts` strips both classes of leakage:
|
||||
|
||||
1. Multi-line stack traces — only the first line (the actual error message) is kept.
|
||||
2. Absolute paths (`/...*.{ts,js,tsx,jsx,mjs,cjs}[:line[:col]]` and `C:\...`) — replaced with `<path>`.
|
||||
|
||||
## The mandatory pattern
|
||||
|
||||
### 1. Building an error response (HTTP / API routes)
|
||||
|
||||
Use `buildErrorBody()` — sanitization is built-in:
|
||||
|
||||
```ts
|
||||
import { buildErrorBody } from "@omniroute/open-sse/utils/error.ts";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
// ... handler logic ...
|
||||
} catch (err) {
|
||||
return new Response(JSON.stringify(buildErrorBody(500, String(err))), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or, for the convenience wrappers in the same module:
|
||||
|
||||
```ts
|
||||
import {
|
||||
errorResponse, // one-shot Response object
|
||||
writeStreamError, // SSE writer
|
||||
createErrorResult, // { success: false, status, response, ... } shape
|
||||
unavailableResponse, // adds Retry-After
|
||||
providerCircuitOpenResponse,
|
||||
modelCooldownResponse,
|
||||
} from "@omniroute/open-sse/utils/error.ts";
|
||||
```
|
||||
|
||||
All of these route through `buildErrorBody` and therefore through `sanitizeErrorMessage`. **You never need to call `sanitizeErrorMessage` manually** when using these helpers.
|
||||
|
||||
### 2. Custom error envelopes (rare)
|
||||
|
||||
When you can't use the helpers above (e.g. the response shape is dictated by an upstream protocol like Connect-RPC), import `sanitizeErrorMessage` directly:
|
||||
|
||||
```ts
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
|
||||
|
||||
const body = JSON.stringify({
|
||||
error: {
|
||||
message: sanitizeErrorMessage(rawMessage),
|
||||
type: "invalid_request_error",
|
||||
code: "",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
This is the only sanctioned way to assemble a custom error body. See `open-sse/executors/cursor.ts::buildErrorResponse` for the reference implementation.
|
||||
|
||||
### 3. Logging vs. responding
|
||||
|
||||
`sanitizeErrorMessage` should **only** wrap the value that crosses the network boundary. Internal logs (`pino`, `console`) should keep the full message, including stack, so operators can debug. Pattern:
|
||||
|
||||
```ts
|
||||
try {
|
||||
// ...
|
||||
} catch (err) {
|
||||
log.error({ err }, "handler failed"); // full err with stack — internal log
|
||||
return errorResponse(500, getErrorMessage(err)); // sanitized — sent to client
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Forbidden patterns
|
||||
|
||||
❌ **Never** put raw exception output in a Response body:
|
||||
|
||||
```ts
|
||||
// BAD: stack trace + file paths reach the client
|
||||
return new Response(JSON.stringify({ error: { message: err.stack || err.message } }), {
|
||||
status: 500,
|
||||
});
|
||||
```
|
||||
|
||||
❌ **Never** roll your own first-line splitter:
|
||||
|
||||
```ts
|
||||
// BAD: forgets to strip absolute paths, may drift from the canonical helper
|
||||
const safe = String(err).split("\n")[0];
|
||||
```
|
||||
|
||||
❌ **Never** sanitize in the route and forget the SSE path. Anything that writes to a stream goes through `writeStreamError` (or its underlying `buildErrorBody`).
|
||||
|
||||
❌ **Never** include `process.cwd()`, `__filename`, `__dirname`, env-derived paths in error messages — they bypass the path regex and reveal the deployment topology.
|
||||
|
||||
## Coverage in CI
|
||||
|
||||
`tests/unit/error-message-sanitization.test.ts` enforces:
|
||||
|
||||
- Every route under `/api/model-combo-mappings/*` returns sanitized bodies on 4xx/5xx.
|
||||
- `sanitizeErrorMessage` strips multi-line stack traces.
|
||||
- `sanitizeErrorMessage` replaces POSIX and Windows absolute paths with `<path>`.
|
||||
- `sanitizeErrorMessage` handles `null`/`undefined`/`Error` instance inputs safely.
|
||||
- `buildErrorBody` never exposes stack traces in its `message` field.
|
||||
|
||||
When adding a new route or executor, copy the assertion pattern from this file. The coverage gate (`npm run test:coverage`) enforces ≥75% statements/lines/functions and ≥70% branches — error paths must be covered.
|
||||
|
||||
## Related controls
|
||||
|
||||
- `js/stack-trace-exposure` CodeQL alerts in `.github/security` should always be **either** fixed via these helpers **or** dismissed with a comment citing this doc.
|
||||
- The `pino` redaction config (`src/lib/log/redaction.ts` — if present) handles structured log redaction separately. This doc covers only the response-message surface.
|
||||
- Upstream-header denylist (`src/shared/constants/upstreamHeaders.ts`) covers header leakage — keep both files aligned when adding a new exfiltration concern.
|
||||
|
||||
## References
|
||||
|
||||
- [CWE-209: Information Exposure Through an Error Message](https://cwe.mitre.org/data/definitions/209.html)
|
||||
- [CodeQL `js/stack-trace-exposure`](https://codeql.github.com/codeql-query-help/javascript/js-stack-trace-exposure/)
|
||||
- [OWASP: Error Handling Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Error_Handling_Cheat_Sheet.html)
|
||||
- Commit centralizing the helper: `1a39c31f` — _fix(security): mask public upstream creds + centralize error sanitization_
|
||||
142
docs/security/PUBLIC_CREDS.md
Normal file
142
docs/security/PUBLIC_CREDS.md
Normal file
@@ -0,0 +1,142 @@
|
||||
---
|
||||
title: "Public Credentials Handling"
|
||||
version: 3.8.0
|
||||
lastUpdated: 2026-05-14
|
||||
---
|
||||
|
||||
# Public Credentials Handling
|
||||
|
||||
> **Source of truth:** `open-sse/utils/publicCreds.ts`
|
||||
> **Tests:** `tests/unit/publicCreds.test.ts`
|
||||
> **Last updated:** 2026-05-14 — v3.8.0
|
||||
> **Audience:** Engineers integrating providers that ship public OAuth client_id / client_secret / Firebase Web API keys in their public CLIs.
|
||||
> **Status:** **MANDATORY** for all new code that embeds upstream identifiers.
|
||||
|
||||
## Why this exists
|
||||
|
||||
Some upstream providers (Gemini CLI, Antigravity CLI, Windsurf / Devin CLI, GitHub Copilot, and similar OAuth-native clients) ship credentials extracted from their **public binaries or web apps**. Google explicitly documents that these are not secrets:
|
||||
|
||||
- [OAuth 2.0 for native apps (PKCE)](https://developers.google.com/identity/protocols/oauth2/native-app) — OAuth client_id / client_secret for installed apps are public; PKCE provides the actual security.
|
||||
- [Firebase API keys](https://firebase.google.com/docs/projects/api-keys) — Web client identifiers are public by design.
|
||||
|
||||
OmniRoute must embed these values so users who do not configure `.env` still get a working OAuth flow out of the box. Without an embedded fallback, the Gemini / Antigravity / Windsurf providers stop working for any user who follows the "just clone and run" path.
|
||||
|
||||
However, literal values like `AIzaSy…`, `GOCSPX-…`, `…apps.googleusercontent.com` are matched by **GitHub Secret Scanning**, **Semgrep**, and similar pattern scanners. Every release becomes a noisy stream of false positives, push protection blocks legitimate commits, and operators stop trusting the alert feed.
|
||||
|
||||
The `open-sse/utils/publicCreds.ts` helper solves both constraints at once:
|
||||
|
||||
- Embeds the public identifier as a **XOR-masked byte sequence** (no scanner pattern in source).
|
||||
- Decodes at runtime via `decodePublicCred` / `resolvePublicCred`.
|
||||
- Detects raw values that already follow well-known prefixes (`AIza`, `GOCSPX-`, `<digits>-<32hex>.apps.googleusercontent.com`, `Iv1.<hex>`) and passes them through unchanged, so users with raw values in their existing `.env` keep working with **zero migration**.
|
||||
|
||||
This is **obfuscation, not encryption.** Anyone reading the source can recover the value — which is fine because the value is public by design. The only goal is to avoid scanner regex matches.
|
||||
|
||||
## The mandatory pattern
|
||||
|
||||
### 1. Adding a new public credential
|
||||
|
||||
When you need to embed a new upstream-provided value that:
|
||||
|
||||
- comes from a public CLI / desktop app / browser bundle, **and**
|
||||
- the upstream provider documents (or treats) it as a public client identifier, **and**
|
||||
- a pattern scanner would otherwise match it (`AIza…`, `GOCSPX-…`, `<digits>-…apps.googleusercontent.com`, etc.),
|
||||
|
||||
…follow this checklist:
|
||||
|
||||
1. Generate the masked byte sequence:
|
||||
|
||||
```bash
|
||||
node --import tsx/esm -e \
|
||||
'import("./open-sse/utils/publicCreds.ts").then(m =>
|
||||
console.log(JSON.stringify(Array.from(
|
||||
Buffer.from(m.encodePublicCred("THE_PUBLIC_VALUE"), "base64")
|
||||
))))'
|
||||
```
|
||||
|
||||
2. Add a new entry to `EMBEDDED_DEFAULTS` in `open-sse/utils/publicCreds.ts` with a **neutral key name** (`<provider>_id`, `<provider>_alt`, `<provider>_fb`, etc.). Do **not** use names like `client_secret` or `api_key` in the helper — those words trigger Semgrep generic-secret rules.
|
||||
|
||||
3. Add a `keyof typeof EMBEDDED_DEFAULTS` to the public type union (it is inferred automatically).
|
||||
|
||||
4. In the consumer code, replace the hardcoded literal with:
|
||||
|
||||
```ts
|
||||
// single env override
|
||||
clientSecret: resolvePublicCred("provider_alt", "PROVIDER_OAUTH_CLIENT_SECRET"),
|
||||
|
||||
// multiple env aliases (first non-empty wins)
|
||||
clientId: resolvePublicCredMulti("provider_id", [
|
||||
"PROVIDER_CLI_OAUTH_CLIENT_ID",
|
||||
"PROVIDER_OAUTH_CLIENT_ID",
|
||||
]),
|
||||
|
||||
// no env override (always embedded default)
|
||||
firebaseApiKey: resolvePublicCred("provider_fb"),
|
||||
```
|
||||
|
||||
5. Remove the literal from `.env.example` (replace with comment-only documentation pointing readers here):
|
||||
|
||||
```dotenv
|
||||
# ── Provider (Google / Firebase / etc.) ──
|
||||
# Public OAuth credentials are baked into the code via
|
||||
# open-sse/utils/publicCreds.ts. Set these vars only to use your own.
|
||||
# PROVIDER_OAUTH_CLIENT_ID=
|
||||
# PROVIDER_OAUTH_CLIENT_SECRET=
|
||||
```
|
||||
|
||||
6. Update `tests/unit/publicCreds.test.ts` to add a shape assertion for the new key (verify format, not literal value — see existing tests for the pattern).
|
||||
|
||||
7. **Never** add `AIza…` / `GOCSPX-…` / `…apps.googleusercontent.com` literals to test files. Use the `FAKE_*` constants built from `.join("")` fragments (see existing tests).
|
||||
|
||||
### 2. Consumers
|
||||
|
||||
- **Read from `resolvePublicCred()` / `resolvePublicCredMulti()` only** — never call `decodePublicCredBytes()` directly outside the helper.
|
||||
- The helper is intentionally cheap (linear byte XOR) and safe to call at module-load time; defaults are computed once.
|
||||
- The env override always wins. If a user sets `PROVIDER_OAUTH_CLIENT_SECRET=GOCSPX-myown`, the helper passes that raw value straight through.
|
||||
|
||||
### 3. Forbidden patterns
|
||||
|
||||
❌ **Never** do any of the following in production code (`src/`, `open-sse/`, `electron/`, `bin/`):
|
||||
|
||||
```ts
|
||||
// BAD: literal value triggers Secret Scanning + Semgrep
|
||||
clientSecret: process.env.PROVIDER_OAUTH_CLIENT_SECRET || "GOCSPX-realvalue",
|
||||
|
||||
// BAD: base64 of the literal — GitHub still detects since Feb/2025
|
||||
clientSecret: process.env.PROVIDER_OAUTH_CLIENT_SECRET ||
|
||||
Buffer.from("R09DU1BYLXJlYWx2YWx1ZQ==", "base64").toString(),
|
||||
|
||||
// BAD: string concatenation that re-assembles the pattern at runtime
|
||||
clientSecret: "GO" + "CS" + "PX-" + "realvalue",
|
||||
|
||||
// BAD: hex/ROT13 encoding — different obfuscation, same risk of detection
|
||||
clientSecret: hexDecode("474f4353..."),
|
||||
```
|
||||
|
||||
These all eventually trip a scanner. Use `resolvePublicCred()`.
|
||||
|
||||
❌ **Never** add literal credentials to `.env.example`. Users who need real upstream values can extract them from the public CLI themselves, or use their own OAuth registration.
|
||||
|
||||
❌ **Never** dismiss a new secret-scanning alert without first checking whether the credential should be moved to this helper.
|
||||
|
||||
## Related controls
|
||||
|
||||
- `RAW_VALUE_PATTERN` in `publicCreds.ts` enumerates the prefixes that trigger passthrough (retrocompat). Extend it only for documented public credential formats, never for proprietary secrets.
|
||||
- `.env.example` lives in CI's `check-env-doc-sync` script — when you remove a var here, make sure the docs match.
|
||||
- The `npm run test:vitest` and `node --import tsx/esm --test tests/unit/publicCreds.test.ts` suites must both stay green.
|
||||
|
||||
## When NOT to use this helper
|
||||
|
||||
This helper is **only** for credentials that are:
|
||||
|
||||
1. Distributed publicly by the upstream provider (CLI binary, browser bundle, official docs).
|
||||
2. Documented or strongly implied to be non-confidential (PKCE-protected, Firebase Web key, similar).
|
||||
|
||||
For everything else — operator-issued tokens, per-tenant secrets, your own OAuth app's client_secret, encryption keys, JWT secrets, database passwords — use **env vars only** (`process.env.FOO`, `||` fallback to empty / explicit error). These belong in `.env` and the [encrypted credentials store](./COMPLIANCE.md), not in source.
|
||||
|
||||
## References
|
||||
|
||||
- [Google: OAuth 2.0 for native apps](https://developers.google.com/identity/protocols/oauth2/native-app)
|
||||
- [Firebase: API keys for client identification](https://firebase.google.com/docs/projects/api-keys)
|
||||
- [GitHub Secret Scanning supported secrets](https://docs.github.com/en/code-security/secret-scanning/introduction/supported-secret-scanning-patterns)
|
||||
- [GitHub: base64 detection for tokens (Feb 2025)](https://github.blog/changelog/2025-02-14-secret-scanning-detects-base64-encoded-github-tokens/)
|
||||
- Commit introducing this helper: `1a39c31f` — _fix(security): mask public upstream creds + centralize error sanitization_
|
||||
@@ -94,8 +94,8 @@ function humanizeBasename(filePath) {
|
||||
}
|
||||
|
||||
function buildFrontmatter(title) {
|
||||
// Quote title with double quotes; escape internal double quotes.
|
||||
const safe = title.replace(/"/g, '\\"');
|
||||
// Quote title with double quotes; escape backslashes first, then double quotes.
|
||||
const safe = title.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
||||
return [
|
||||
`---`,
|
||||
`title: "${safe}"`,
|
||||
|
||||
@@ -96,9 +96,13 @@ const DOC_TO_SUBFOLDER = {
|
||||
};
|
||||
|
||||
// Build alternation regex (longest-first) of file basenames we know about.
|
||||
// Escape regex metacharacters (including backslash) defensively, even though
|
||||
// the source list is internal — keeps CodeQL happy and future-proofs against
|
||||
// names like "FOO\BAR.md".
|
||||
const RE_META = /[\\^$.*+?()[\]{}|]/g;
|
||||
const FILES_ALT = Object.keys(DOC_TO_SUBFOLDER)
|
||||
.sort((a, b) => b.length - a.length)
|
||||
.map((s) => s.replace(/\./g, "\\."))
|
||||
.map((s) => s.replace(RE_META, "\\$&"))
|
||||
.join("|");
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
@@ -50,7 +50,8 @@ function asRecords(map: Record<string, ProviderRecord>): ProviderRecord[] {
|
||||
|
||||
function escapeCell(value: string | undefined): string {
|
||||
if (!value) return "—";
|
||||
return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
||||
// Escape backslash first so the subsequent escapes don't double-escape it.
|
||||
return value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\n/g, " ");
|
||||
}
|
||||
|
||||
function row(p: ProviderRecord, category: string): string {
|
||||
|
||||
@@ -43,15 +43,19 @@ async function updateNssDatabases(
|
||||
certPath: string | null,
|
||||
action: "add" | "delete" = "add"
|
||||
): Promise<void> {
|
||||
const certName = "OmniRoute MITM Root CA";
|
||||
|
||||
// Pass the runtime values via environment variables instead of string
|
||||
// interpolation. The shell receives them through its env and dereferences
|
||||
// with "$CERT_PATH" / "$CERT_NAME" / "$ACTION", so any shell metacharacters
|
||||
// they may contain stay inside the quoted argument — eliminating the
|
||||
// command-injection surface flagged by CodeQL js/shell-command-injection.
|
||||
const script = `
|
||||
set -u
|
||||
if ! command -v certutil &> /dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
DIRS="$HOME/.pki/nssdb $HOME/snap/chromium/current/.pki/nssdb"
|
||||
|
||||
|
||||
if [ -d "$HOME/.mozilla/firefox" ]; then
|
||||
for profile in "$HOME"/.mozilla/firefox/*/; do
|
||||
if [ -f "\${profile}cert9.db" ] || [ -f "\${profile}cert8.db" ]; then
|
||||
@@ -70,19 +74,31 @@ async function updateNssDatabases(
|
||||
|
||||
for db in $DIRS; do
|
||||
if [ -d "$db" ]; then
|
||||
if [ "${action}" = "add" ]; then
|
||||
certutil -d sql:"$db" -A -t "C,," -n "${certName}" -i "${certPath}" 2>/dev/null || \\
|
||||
certutil -d "$db" -A -t "C,," -n "${certName}" -i "${certPath}" 2>/dev/null || true
|
||||
if [ "$ACTION" = "add" ]; then
|
||||
certutil -d sql:"$db" -A -t "C,," -n "$CERT_NAME" -i "$CERT_PATH" 2>/dev/null || \\
|
||||
certutil -d "$db" -A -t "C,," -n "$CERT_NAME" -i "$CERT_PATH" 2>/dev/null || true
|
||||
else
|
||||
certutil -d sql:"$db" -D -n "${certName}" 2>/dev/null || \\
|
||||
certutil -d "$db" -D -n "${certName}" 2>/dev/null || true
|
||||
certutil -d sql:"$db" -D -n "$CERT_NAME" 2>/dev/null || \\
|
||||
certutil -d "$db" -D -n "$CERT_NAME" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
`;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
exec(script, { shell: "/bin/bash" }, () => resolve());
|
||||
exec(
|
||||
script,
|
||||
{
|
||||
shell: "/bin/bash",
|
||||
env: {
|
||||
...process.env,
|
||||
CERT_NAME: "OmniRoute MITM Root CA",
|
||||
CERT_PATH: certPath || "",
|
||||
ACTION: action,
|
||||
},
|
||||
},
|
||||
() => resolve()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user