mirror of
https://github.com/stablyai/orca.git
synced 2026-10-04 00:02:21 +00:00
fix(ssh): budget directory traversal in Include globs
Adds MAX_INCLUDE_GLOB_TRAVERSAL limit to prevent pathological glob patterns from walking excessive directories. When a single Include glob walks more than the budget (1024 directories), stop expanding and mark the result as incomplete.
This commit is contained in:
@@ -180,6 +180,19 @@ describe('SSH config Include completeness', () => {
|
||||
|
||||
expect(expandSshConfigIncludes(configPath).fullyExpanded).toBe(true)
|
||||
})
|
||||
|
||||
it('stops a glob that walks too many directories and marks it incomplete', () => {
|
||||
const home = makeTemporaryHome()
|
||||
const configPath = writeFile(home, '.ssh/config', 'Include conf.d/*/config\n')
|
||||
writeFile(home, '.ssh/conf.d/aaa/config', 'Host early\n HostName early.example.com\n')
|
||||
for (let index = 0; index < 1100; index += 1) {
|
||||
mkdirSync(join(home, '.ssh', 'conf.d', `dir-${index}`))
|
||||
}
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
expect(expandSshConfigIncludes(configPath).fullyExpanded).toBe(false)
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringContaining('walks more than'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('SSH config Include warnings', () => {
|
||||
|
||||
@@ -43,6 +43,8 @@ type ResolvedIncludePaths = {
|
||||
}
|
||||
|
||||
const MAX_INCLUDE_GLOB_MATCHES = 256
|
||||
// Caps the directories a single Include glob may walk on the main process before we stop collecting.
|
||||
const MAX_INCLUDE_GLOB_TRAVERSAL = 1024
|
||||
const MAX_INCLUDE_FILE_BYTES = 1024 * 1024
|
||||
|
||||
export function expandSshConfigIncludes(configPath: string): SshConfigExpansion {
|
||||
@@ -247,11 +249,19 @@ function resolveIncludePaths(
|
||||
const absolutePattern = resolveIncludePatternPath(withTokens, context)
|
||||
if (hasGlobPattern(absolutePattern)) {
|
||||
try {
|
||||
const matches = globSync(absolutePattern).sort((left, right) => left.localeCompare(right))
|
||||
if (matches.length > MAX_INCLUDE_GLOB_MATCHES) {
|
||||
const { matches, traversalLimited } = globWithTraversalLimit(absolutePattern)
|
||||
matches.sort((left, right) => left.localeCompare(right))
|
||||
if (traversalLimited) {
|
||||
console.warn(
|
||||
`[ssh] Include pattern "${logTarget(absolutePattern)}" walks more than ${MAX_INCLUDE_GLOB_TRAVERSAL} directories; processing matches found so far`
|
||||
)
|
||||
} else if (matches.length > MAX_INCLUDE_GLOB_MATCHES) {
|
||||
console.warn(
|
||||
`[ssh] Include pattern "${logTarget(absolutePattern)}" matched ${matches.length} files; processing first ${MAX_INCLUDE_GLOB_MATCHES}`
|
||||
)
|
||||
}
|
||||
if (traversalLimited || matches.length > MAX_INCLUDE_GLOB_MATCHES) {
|
||||
// Already incomplete, so a completeness proof would only add another scan.
|
||||
context.fullyExpanded = false
|
||||
return {
|
||||
paths: matches.slice(0, MAX_INCLUDE_GLOB_MATCHES),
|
||||
@@ -291,6 +301,25 @@ function resolveIncludePaths(
|
||||
}
|
||||
}
|
||||
|
||||
/** Stops descending once the walk exceeds its budget, keeping the matches already found. */
|
||||
function globWithTraversalLimit(pattern: string): {
|
||||
matches: string[]
|
||||
traversalLimited: boolean
|
||||
} {
|
||||
let remaining = MAX_INCLUDE_GLOB_TRAVERSAL
|
||||
let traversalLimited = false
|
||||
const matches = globSync(pattern, {
|
||||
exclude: () => {
|
||||
remaining -= 1
|
||||
if (remaining < 0) {
|
||||
traversalLimited = true
|
||||
}
|
||||
return traversalLimited
|
||||
}
|
||||
})
|
||||
return { matches, traversalLimited }
|
||||
}
|
||||
|
||||
function getCanonicalPath(
|
||||
filePath: string,
|
||||
context: IncludeExpansionContext,
|
||||
|
||||
Reference in New Issue
Block a user