mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 06:28:59 +02:00
fix(design-detect): an engine is a file named impeccable outside the project; DOM dumps scan without inline ignores
Second review cycle, security + checklist: - IMPECCABLE_BIN=/bin/sh (or node) was READY, and `detect` with cwd=repoRoot made the interpreter run the repository's own `detect` file. Every engine candidate (env override, PATH entry, cache, sibling) is now judged by the realpath of the FILE and must be named impeccable[.exe]; PATH and cache candidates that resolve into the repository are skipped like the others. "Inside the project" means the repository, or cwd when cwd is a project directory: HOME and its ancestors are exempt, so a URL-mode review launched from HOME still finds the HOME-rooted installs. - A base for --changed that starts with `-` was spliced into git argv (`--output=<file>` made git write a file and report no changes); an option- like or missing base is DETECT_REFUSED (not a ref name), exit 1, and the parser no longer defaults a missing value to main. - DOM dumps are the audited page's bytes, so an in-file `impeccable-disable` comment there is page-controlled: batches under the designs root run with --no-inline-ignores, repository batches keep the project's own ignores. - neutralizeSentinels covers the shapes it missed (bare sentinels such as DETECT_TOP total= and IMPECCABLE_DISABLED, the DETECT_EXIT_CODE= echo, the `[rule-id] impact=` group header) in one precompiled alternation instead of 37 replaceAll passes per field; only kept findings are normalized, and the summary's total stays the engine's count. - The minimal engine environment compares keys case-insensitively on Windows (process.env enumerates Path, SystemRoot there) and passes PATHEXT, COMSPEC, HOMEDRIVE, HOMEPATH, PROGRAMDATA. - Bare 64s move into DETECT_LIMITS; the unused SentinelName type is gone; the header states the directory-target contract (the engine's own walk). Tests: an interpreter as IMPECCABLE_BIN never runs the repo's detect file; a PATH symlink into the repository is never READY; option-like and empty bases are refused with no file written; the designs-root batch carries --no-inline-ignores and the repo batch does not; the identity label is deterministic per binary; the bare-sentinel and header shapes are neutralized; the installed fake engine works without IMPECCABLE_FAKE_OUTPUT (the helper copies the sample beside it); two tests clean up in finally. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
da6f0ff2f6
commit
b2e67d0097
@@ -60,7 +60,6 @@ export const SENTINEL = {
|
||||
ENGINE_STDERR: 'ENGINE_STDERR',
|
||||
} as const;
|
||||
|
||||
export type SentinelName = keyof typeof SENTINEL;
|
||||
|
||||
/**
|
||||
* Sentinels whose line explains itself after the colon (a path, a version, a
|
||||
@@ -107,21 +106,32 @@ export const DETECT_LIMITS = {
|
||||
/** git subprocess budgets inside the wrapper */
|
||||
gitTimeoutMs: 30_000,
|
||||
gitMaxBuffer: 64 * 1024 * 1024,
|
||||
field: { id: 64, message: 120, snippet: 120, value: 200, file: 4096, diagnostic: 400, refusedTarget: 200, parseErrorPreview: 80, internalError: 300 },
|
||||
field: { id: 64, engineVersion: 64, message: 120, snippet: 120, value: 200, file: 4096, diagnostic: 400, refusedTarget: 200, parseErrorPreview: 80, internalError: 300 },
|
||||
} as const;
|
||||
|
||||
const escapeRe = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
/**
|
||||
* Break any sentinel or fence marker that appears INSIDE engine-derived text,
|
||||
* so page content echoed through a finding cannot close the untrusted envelope
|
||||
* or forge a probe line. Inserts a zero-width space after the first character
|
||||
* (the same technique browse/src/content-security.ts uses for its markers).
|
||||
* One pass over every shape the agent reads as gstack's own voice: the two fence
|
||||
* markers, any sentinel word (whole word, colon or not: `DETECT_TOP total=` and
|
||||
* `IMPECCABLE_DISABLED` are printed bare), and the `[rule-id] impact=` group
|
||||
* header. Longest sentinel first so DETECT_EXIT_CODE is not split at DETECT_EXIT.
|
||||
*/
|
||||
const NEUTRALIZE_RE = new RegExp(
|
||||
[escapeRe(UNTRUSTED_BEGIN), escapeRe(UNTRUSTED_END),
|
||||
'\\b(?:' + [...new Set(Object.values(SENTINEL))].sort((a, b) => b.length - a.length).map(escapeRe).join('|') + ')\\b',
|
||||
'\\[(?=[a-z0-9-]+\\] impact=)'].join('|'), 'g');
|
||||
|
||||
/**
|
||||
* Break any sentinel, fence marker, or group header that appears INSIDE
|
||||
* engine-derived text, so page content echoed through a finding cannot close
|
||||
* the untrusted envelope or forge a probe line. Inserts a zero-width space after
|
||||
* the first character (the same technique browse/src/content-security.ts uses
|
||||
* for its markers). One precompiled alternation: this runs on four fields of
|
||||
* every kept finding.
|
||||
*/
|
||||
export function neutralizeSentinels(s: string): string {
|
||||
const zw = '\u200b';
|
||||
let out = s.replaceAll(UNTRUSTED_BEGIN, UNTRUSTED_BEGIN[0] + zw + UNTRUSTED_BEGIN.slice(1))
|
||||
.replaceAll(UNTRUSTED_END, UNTRUSTED_END[0] + zw + UNTRUSTED_END.slice(1));
|
||||
for (const v of Object.values(SENTINEL)) out = out.replaceAll(v + ':', v[0] + zw + v.slice(1) + ':');
|
||||
return out;
|
||||
return s.replace(NEUTRALIZE_RE, m => m[0] + '\u200b' + m.slice(1));
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user