fix(llm-security): B1 pathguard regex — match multi-segment .env.*.*
The previous ENV regex `/[\\/]\.env\.[a-z]+$/` only matched a single lowercase segment after `.env`. Multi-segment and mixed-case variants such as `.env.production.local.backup`, `.env.stage-1.local`, and `.env.CI.secret` slipped past the hook. Replaced with `/[\\/]\.env(\.[A-Za-z0-9._-]+)*$/` which matches `.env` plus any number of dot-separated alphanumeric/dot/hyphen/underscore segments. `.envrc` (direnv config, no dot separator) is still allowed. Addresses critical review 2026-04-20 §2 B1 (HIGH). Tests: 7 added (6 new multi-segment BLOCK cases + 1 .envrc ALLOW). All 1494 tests pass.
This commit is contained in:
parent
a6e2c939ef
commit
751f1199c8
2 changed files with 51 additions and 4 deletions
|
|
@ -17,11 +17,15 @@ import { getPolicyValue } from '../../scanners/lib/policy-loader.mjs';
|
|||
// Sensitive path patterns — 8 categories
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Category 1: Environment files */
|
||||
/** Category 1: Environment files
|
||||
* Matches `.env` and any multi-segment suffix after it, e.g.
|
||||
* `.env.local`, `.env.production.local.backup`, `.env.stage-1.local`,
|
||||
* `.env.CI.secret`. Does NOT match `.envrc` (direnv) — no dot separator.
|
||||
* v7.1.0 B1 fix: previous regex `/[\\/]\.env\.[a-z]+$/` only matched a
|
||||
* single lowercase segment after `.env`.
|
||||
*/
|
||||
const ENV_PATTERNS = [
|
||||
/[\\/]\.env$/,
|
||||
/[\\/]\.env\.[a-z]+$/, // .env.local, .env.production, etc.
|
||||
/[\\/]\.env\.local$/,
|
||||
/[\\/]\.env(\.[A-Za-z0-9._-]+)*$/,
|
||||
];
|
||||
|
||||
/** Category 2: SSH directory */
|
||||
|
|
|
|||
|
|
@ -36,6 +36,43 @@ describe('pre-write-pathguard — BLOCK cases', () => {
|
|||
assert.match(result.stderr, /PATH GUARD/);
|
||||
});
|
||||
|
||||
// B1 regression — multi-segment .env.*.*.* must be blocked (v7.1.0)
|
||||
it('blocks a write to .env.production.local.backup (multi-segment env)', async () => {
|
||||
const result = await runHook(SCRIPT, writePayload('/project/.env.production.local.backup'));
|
||||
assert.equal(result.code, 2);
|
||||
assert.match(result.stderr, /PATH GUARD/);
|
||||
});
|
||||
|
||||
it('blocks a write to .env.dev.local.old (multi-segment env)', async () => {
|
||||
const result = await runHook(SCRIPT, writePayload('/project/.env.dev.local.old'));
|
||||
assert.equal(result.code, 2);
|
||||
assert.match(result.stderr, /PATH GUARD/);
|
||||
});
|
||||
|
||||
it('blocks a write to .env.prod.local.bak (multi-segment env)', async () => {
|
||||
const result = await runHook(SCRIPT, writePayload('/project/.env.prod.local.bak'));
|
||||
assert.equal(result.code, 2);
|
||||
assert.match(result.stderr, /PATH GUARD/);
|
||||
});
|
||||
|
||||
it('blocks a write to .env.stage-1.local (hyphen + digit in segment)', async () => {
|
||||
const result = await runHook(SCRIPT, writePayload('/project/.env.stage-1.local'));
|
||||
assert.equal(result.code, 2);
|
||||
assert.match(result.stderr, /PATH GUARD/);
|
||||
});
|
||||
|
||||
it('blocks a write to .env.CI.secret (uppercase segment)', async () => {
|
||||
const result = await runHook(SCRIPT, writePayload('/project/.env.CI.secret'));
|
||||
assert.equal(result.code, 2);
|
||||
assert.match(result.stderr, /PATH GUARD/);
|
||||
});
|
||||
|
||||
it('blocks a write to .env.A.B.C.D (many short uppercase segments)', async () => {
|
||||
const result = await runHook(SCRIPT, writePayload('/project/.env.A.B.C.D'));
|
||||
assert.equal(result.code, 2);
|
||||
assert.match(result.stderr, /PATH GUARD/);
|
||||
});
|
||||
|
||||
it('blocks a write to .ssh/id_rsa (SSH directory)', async () => {
|
||||
const result = await runHook(SCRIPT, writePayload('/home/user/.ssh/id_rsa'));
|
||||
assert.equal(result.code, 2);
|
||||
|
|
@ -117,6 +154,12 @@ describe('pre-write-pathguard — ALLOW cases', () => {
|
|||
assert.equal(result.code, 0);
|
||||
});
|
||||
|
||||
// B1 negative — direnv's .envrc (no dot-suffix) must not be blocked (v7.1.0)
|
||||
it('allows a write to .envrc (direnv config, not a dotenv file)', async () => {
|
||||
const result = await runHook(SCRIPT, writePayload('/project/.envrc'));
|
||||
assert.equal(result.code, 0);
|
||||
});
|
||||
|
||||
it('allows a write when file_path is empty', async () => {
|
||||
const result = await runHook(SCRIPT, { tool_name: 'Write', tool_input: { file_path: '', content: 'x' } });
|
||||
assert.equal(result.code, 0);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue