/*** Git can be weaponized for sandbox escape via two vectors:* 1. Bare-repo attack: if cwd contains HEAD + objects/ + refs/ but no valid* .git/HEAD, Git treats cwd as a bare repository and runs hooks from cwd.* 2. Git-internal write + git: a compound command creates HEAD/objects/refs/* hooks/ then runs git — the git subcommand executes the freshly-created* malicious hooks.*/import { basename, posix, resolve, sep } from 'path'import { getCwd } from '../../utils/cwd.js'import { PS_TOKENIZER_DASH_CHARS } from '../../utils/powershell/parser.js'/*** If a normalized path starts with `../<cwd-basename>/`, it re-enters cwd* via the parent — resolve it to the cwd-relative form. posix.normalize* preserves leading `..` (no cwd context), so `../project/hooks` with* cwd=/x/project stays `../project/hooks` and misses the `hooks/` prefix* match even though it resolves to the same directory at runtime.* Check/use divergence: validator sees `../project/hooks`, PowerShell* resolves against cwd to `hooks`.*/function resolveCwdReentry(normalized: string): string {if (!normalized.startsWith('../')) return normalizedconst cwdBase = basename(getCwd()).toLowerCase()if (!cwdBase) return normalized// Iteratively strip `../<cwd-basename>/` pairs (handles `../../p/p/hooks`// when cwd has repeated basename segments is unlikely, but one-level is// the common attack).const prefix = '../' + cwdBase + '/'let s = normalizedwhile (s.startsWith(prefix)) {s = s.slice(prefix.length)}// Also handle exact `../<cwd-basename>` (no trailing slash)if (s === '../' + cwdBase) return '.'return s}/*** Normalize PS arg text → canonical path for git-internal matching.* Order matters: structural strips first (colon-bound param, quotes,* backtick escapes, provider prefix, drive-relative prefix), then NTFS* per-component trailing-strip (spaces always; dots only if not `./..`* after space-strip), then posix.normalize (resolves `..`, `.`, `//`),* then case-fold.*/function normalizeGitPathArg(arg: string): string {let s = arg// Normalize parameter prefixes: dash chars (–, —, ―) and forward-slash// (PS 5.1). /Path:hooks/pre-commit → extract colon-bound value. (bug #28)if (s.length > 0 && (PS_TOKENIZER_DASH_CHARS.has(s[0]!) || s[0] === '/')) {const c = s.indexOf(':', 1)if (c > 0) s = s.slice(c + 1)}s = s.replace(/^['"]|['"]$/g, '')s = s.replace(/`/g, '')// PS provider-qualified path: FileSystem::hooks/pre-commit → hooks/pre-commit// Also handles fully-qualified form: Microsoft.PowerShell.Core\FileSystem::paths = s.replace(/^(?:[A-Za-z0-9_.]+\\){0,3}FileSystem::/i, '')// Drive-relative C:foo (no separator after colon) is cwd-relative on that// drive. C:\foo (WITH separator) is absolute and must NOT match — the// negative lookahead preserves it.s = s.replace(/^[A-Za-z]:(?![/\\])/, '')s = s.replace(/\\/g, '/')// Win32 CreateFileW per-component: iteratively strip trailing spaces,// then trailing dots, stopping if the result is `.` or `..` (special).// `.. ` → `..`, `.. .` → `..`, `...` → '' → `.`, `hooks .` → `hooks`.// Originally-'' (leading slash split) stays '' (absolute-path marker).s = s.split('/').map(c => {if (c === '') return clet prevdo {prev = cc = c.replace(/ +$/, '')if (c === '.' || c === '..') return cc = c.replace(/\.+$/, '')} while (c !== prev)return c || '.'}).join('/')s = posix.normalize(s)if (s.startsWith('./')) s = s.slice(2)return s.toLowerCase()}const GIT_INTERNAL_PREFIXES = ['head', 'objects', 'refs', 'hooks'] as const/*** SECURITY: Resolve a normalized path that escapes cwd (leading `../` or* absolute) against the actual cwd, then check if it lands back INSIDE cwd.* If so, strip cwd and return the cwd-relative remainder for prefix matching.* If it lands outside cwd, return null (genuinely external — path-validation's* concern). Covers `..\<cwd-basename>\HEAD` and `C:\<full-cwd>\HEAD` which* posix.normalize alone cannot resolve (it leaves leading `..` as-is).** This is the SOLE guard for the bare-repo HEAD attack. path-validation's* DANGEROUS_FILES deliberately excludes bare `HEAD` (false-positive risk* on legitimate non-git files named HEAD) and DANGEROUS_DIRECTORIES* matches per-segment `.git` only — so `<cwd>/HEAD` passes that layer.* The cwd-resolution here is load-bearing; do not remove without adding* an alternative guard.*/function resolveEscapingPathToCwdRelative(n: string): string | null {const cwd = getCwd()// Reconstruct a platform-resolvable path from the posix-normalized form.// `n` has forward slashes (normalizeGitPathArg converted \\ → /); resolve()// handles forward slashes on Windows.const abs = resolve(cwd, n)const cwdWithSep = cwd.endsWith(sep) ? cwd : cwd + sep// Case-insensitive comparison: normalizeGitPathArg lowercased `n`, so// resolve() output has lowercase components from `n` but cwd may be// mixed-case (e.g. C:\Users\...). Windows paths are case-insensitive.const absLower = abs.toLowerCase()const cwdLower = cwd.toLowerCase()const cwdWithSepLower = cwdWithSep.toLowerCase()if (absLower === cwdLower) return '.'if (!absLower.startsWith(cwdWithSepLower)) return nullreturn abs.slice(cwdWithSep.length).replace(/\\/g, '/').toLowerCase()}function matchesGitInternalPrefix(n: string): boolean {if (n === 'head' || n === '.git') return trueif (n.startsWith('.git/') || /^git~\d+($|\/)/.test(n)) return truefor (const p of GIT_INTERNAL_PREFIXES) {if (p === 'head') continueif (n === p || n.startsWith(p + '/')) return true}return false}/*** True if arg (raw PS arg text) resolves to a git-internal path in cwd.* Covers both bare-repo paths (hooks/, refs/) and standard-repo paths* (.git/hooks/, .git/config).*/export function isGitInternalPathPS(arg: string): boolean {const n = resolveCwdReentry(normalizeGitPathArg(arg))if (matchesGitInternalPrefix(n)) return true// SECURITY: leading `../` or absolute paths that resolveCwdReentry and// posix.normalize couldn't fully resolve. Resolve against actual cwd — if// the result lands back in cwd at a git-internal location, the guard must// still fire.if (n.startsWith('../') || n.startsWith('/') || /^[a-z]:/.test(n)) {const rel = resolveEscapingPathToCwdRelative(n)if (rel !== null && matchesGitInternalPrefix(rel)) return true}return false}/*** True if arg resolves to a path inside .git/ (standard-repo metadata dir).* Unlike isGitInternalPathPS, does NOT match bare-repo-style root-level* `hooks/`, `refs/` etc. — those are common project directory names.*/export function isDotGitPathPS(arg: string): boolean {const n = resolveCwdReentry(normalizeGitPathArg(arg))if (matchesDotGitPrefix(n)) return true// SECURITY: same cwd-resolution as isGitInternalPathPS — catch// `..\<cwd-basename>\.git\hooks\pre-commit` that lands back in cwd.if (n.startsWith('../') || n.startsWith('/') || /^[a-z]:/.test(n)) {const rel = resolveEscapingPathToCwdRelative(n)if (rel !== null && matchesDotGitPrefix(rel)) return true}return false}function matchesDotGitPrefix(n: string): boolean {if (n === '.git' || n.startsWith('.git/')) return true// NTFS 8.3 short names: .git becomes GIT~1 (or GIT~2, etc. if multiple// dotfiles start with "git"). normalizeGitPathArg lowercases, so check// for git~N as the first component.return /^git~\d+($|\/)/.test(n)}
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。