mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 14:38:59 +02:00
fix(redact): stop reporting .env.local as an internal hostname
`internal.hostname` ends in `.local|.prod|.staging|…`, so `.env.local` matches on `env.local` and a dotenv FILENAME is reported as a leaked internal host. The collision is not exotic. It fires on `--env-file=.env.local` in an npm script, `.env.staging` in a README, `.env.prod` in a .gitignore — ordinary lines on branches that leak nothing. Measured on one private repo, three of four MEDIUM findings in a routine push were this, and the fourth was a deleted localhost URL. That ratio is the real cost: a scanner that reports package.json is one people learn to skim, and skimming is how the HIGH finding it exists for gets missed. The guard follows the `insideUuid` precedent and stays deliberately narrow — it exempts only a span beginning `env.` immediately preceded by a dot, i.e. the literal `.env.<suffix>` form. `api.corp.local`, `build-7.internal` and `myenv.local` all still report. The test pins both directions, and the negative controls are the point: an exemption written as "any span ending .local" would pass the dotenv half while quietly gutting the pattern for every real host. Verified red/green — with the validate hook removed, exactly the 6 dotenv cases fail and all 9 real-host controls still pass.
This commit is contained in:
@@ -298,6 +298,33 @@ const UUID_RE = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-
|
||||
* this stays cheap on a multi-megabyte buffer. */
|
||||
const UUID_CONTEXT_CHARS = 40;
|
||||
|
||||
/**
|
||||
* True when an `internal.hostname` span is really the tail of a dotenv
|
||||
* FILENAME (`.env.local`, `.env.staging`, `.env.prod`) rather than a host.
|
||||
*
|
||||
* The hostname pattern ends in `.local|.prod|.staging|…`, so `.env.local`
|
||||
* matches on `env.local` — a false positive on one of the most commonly
|
||||
* committed filenames there is. It arrives via npm scripts
|
||||
* (`--env-file=.env.local`), READMEs, `.gitignore` and setup docs, i.e. on
|
||||
* ordinary branches that leak nothing, which is the noise that teaches people
|
||||
* to skim past MEDIUM findings.
|
||||
*
|
||||
* Deliberately narrow, in the same spirit as `insideUuid`: it exempts ONLY a
|
||||
* span beginning `env.` that is immediately preceded by a dot — the literal
|
||||
* `.env.<suffix>` form. A real host still reports, `api.corp` and
|
||||
* `build-7.internal` included, and so does `myenv.local`, which is not a
|
||||
* dotenv file.
|
||||
*/
|
||||
export function isDotenvFilename(match: RegExpExecArray): boolean {
|
||||
const input = match.input ?? "";
|
||||
const span = match[1] ?? match[0];
|
||||
if (!/^env\./i.test(span)) return false;
|
||||
// Mirror the engine: capture group 1 when present, else the whole match.
|
||||
const spanStartInMatch = match[1] !== undefined ? match[0].indexOf(match[1]) : 0;
|
||||
const spanStart = match.index + Math.max(0, spanStartInMatch);
|
||||
return spanStart > 0 && input[spanStart - 1] === ".";
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the matched span sits ENTIRELY inside a UUID.
|
||||
*
|
||||
@@ -751,6 +778,8 @@ export const PATTERNS: RedactPattern[] = [
|
||||
category: "internal",
|
||||
description: "Internal hostname (*.internal/.corp/.local/.prod/.staging)",
|
||||
regex: /\b([a-z0-9][a-z0-9\-]*\.(?:internal|corp|local|lan|prod|staging))\b/i,
|
||||
// `.env.local` and friends are filenames, not hosts. See isDotenvFilename.
|
||||
validate: (_span, match) => !isDotenvFilename(match),
|
||||
},
|
||||
{
|
||||
id: "internal.url_private",
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* internal.hostname vs dotenv FILENAMES.
|
||||
*
|
||||
* `.env.local` ends in `.local`, so the internal-hostname pattern matches on
|
||||
* `env.local` and reports a filename as a leaked internal host. This is not an
|
||||
* exotic collision: `--env-file=.env.local` in an npm script, `.env.staging` in
|
||||
* a README, `.env.prod` in a .gitignore. It fires on branches that leak
|
||||
* nothing, and a scanner that cries wolf on package.json is a scanner people
|
||||
* learn to skim past — which costs far more than the finding was ever worth.
|
||||
*
|
||||
* The guard has to stay narrow, so this file pins BOTH directions. The
|
||||
* negative controls are the point: an exemption written as "any span ending
|
||||
* .local" would pass the dotenv half while quietly gutting the pattern for
|
||||
* every real host.
|
||||
*/
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { scan } from "../lib/redact-engine";
|
||||
import { isDotenvFilename } from "../lib/redact-patterns";
|
||||
|
||||
const flagsHost = (s: string): boolean =>
|
||||
scan(s, { repoVisibility: "private" }).findings.some((f) => f.id === "internal.hostname");
|
||||
|
||||
describe("internal.hostname — real internal hosts stay flagged", () => {
|
||||
const REAL_HOSTS: [string, string][] = [
|
||||
[".internal", "curl http://build-7.internal/health"],
|
||||
[".corp", "ssh jump.corp"],
|
||||
[".local", "ping printer.local"],
|
||||
[".lan", "nas.lan is down"],
|
||||
[".prod", "deploy to shipping.prod now"],
|
||||
[".staging", "hit api.staging first"],
|
||||
["multi-label, dotted prefix", "host: api.corp.local"],
|
||||
["not a dotenv file", "myenv.local resolves"],
|
||||
["env as a real subdomain", "https://env.prod//status"],
|
||||
];
|
||||
for (const [label, input] of REAL_HOSTS) {
|
||||
test(label, () => {
|
||||
expect(flagsHost(input)).toBe(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("internal.hostname — dotenv filenames are not hosts", () => {
|
||||
const DOTENV: [string, string][] = [
|
||||
["npm script", '"dev": "tsx --env-file=.env.local scripts/x.ts"'],
|
||||
["bare filename", "copy .env.example to .env.local"],
|
||||
["staging", "secrets live in .env.staging"],
|
||||
["prod", "never commit .env.prod"],
|
||||
["gitignore line", ".env.local"],
|
||||
["path prefix", "apps/web/.env.local"],
|
||||
];
|
||||
for (const [label, input] of DOTENV) {
|
||||
test(label, () => {
|
||||
expect(flagsHost(input)).toBe(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("isDotenvFilename — unit", () => {
|
||||
const matchFor = (input: string): RegExpExecArray => {
|
||||
const re = /\b([a-z0-9][a-z0-9\-]*\.(?:internal|corp|local|lan|prod|staging))\b/i;
|
||||
const m = re.exec(input);
|
||||
if (!m) throw new Error(`pattern did not match: ${input}`);
|
||||
return m;
|
||||
};
|
||||
|
||||
test("exempts a dot-prefixed env filename", () => {
|
||||
expect(isDotenvFilename(matchFor("--env-file=.env.local"))).toBe(true);
|
||||
});
|
||||
|
||||
test("does not exempt env.local without the leading dot", () => {
|
||||
expect(isDotenvFilename(matchFor("host env.local here"))).toBe(false);
|
||||
});
|
||||
|
||||
test("does not exempt a dot-prefixed host that is not env", () => {
|
||||
expect(isDotenvFilename(matchFor("api.corp.local"))).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user