mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 08:03:33 +00:00
fix(preload): stop the sandbox guard failing on quoted and commented text
The three detection regexes ran over raw source, so a preload containing
`const help = "Call require('fs')"` failed the build with a node-builtin
violation, and a comment mentioning a module call did the same — nothing
minifies preload chunks, so comments survive into the emitted output.
A safe preload could not build.
Mask lexical spans first: one pass marks every comment and string
character while preserving offsets, and a match is vetoed only when its
module keyword lands on a masked offset. The regexes still run over the
original source, so real specifiers are unaffected, and template
interpolations stay unmasked — `${require('fs')}` is still a violation.
Rollup's resolved import lists remain the authoritative check.
Costs 104 production lines against a program rule that wants strictly
net-negative code. A dependency-free lexer cannot be written smaller, and
the alternative — trusting chunk metadata alone — would drop detection of
the renamed and non-literal `require` forms the guard exists to catch.
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -110,6 +110,71 @@ describe('preload sandbox guard detection', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Why: the case above only covers identifier prefixes, a quoted *static import statement* off
|
||||
// statement position, and member-access `require`. None of them exercise call syntax inside a
|
||||
// literal, so the guard used to fail the build on comments and ordinary strings.
|
||||
describe('preload sandbox guard lexical masking', () => {
|
||||
it('ignores a module call quoted inside a string literal', () => {
|
||||
expect(findPreloadSandboxViolations(`const help = "Call require('fs')"`)).toEqual([])
|
||||
})
|
||||
|
||||
it('ignores a module call in a line comment, which survives an unminified chunk', () => {
|
||||
expect(findPreloadSandboxViolations("// use require('fs')\nexport {}")).toEqual([])
|
||||
})
|
||||
|
||||
it('ignores a module call in a block comment', () => {
|
||||
expect(findPreloadSandboxViolations("/* see require('fs')\n and import('path') */")).toEqual(
|
||||
[]
|
||||
)
|
||||
})
|
||||
|
||||
it('ignores a module call inside a template literal', () => {
|
||||
expect(findPreloadSandboxViolations('const t = `see import("path") here`')).toEqual([])
|
||||
})
|
||||
|
||||
it('ignores a fake require after an escaped quote, which naive masking would end early', () => {
|
||||
expect(findPreloadSandboxViolations('const s = "a\\"require(\'fs\')\\"b"')).toEqual([])
|
||||
})
|
||||
|
||||
it('still flags a real request inside a template interpolation', () => {
|
||||
expect(findPreloadSandboxViolations('const t = `${require("fs")}`')).toEqual([
|
||||
{ kind: 'node-builtin', detail: 'fs' }
|
||||
])
|
||||
})
|
||||
|
||||
it('still flags a real request nested in an interpolated object literal', () => {
|
||||
expect(findPreloadSandboxViolations('const t = `${({ a: require("net") }).a}` + "x"')).toEqual([
|
||||
{ kind: 'node-builtin', detail: 'net' }
|
||||
])
|
||||
})
|
||||
|
||||
it('still flags a real request on the line after a comment', () => {
|
||||
expect(findPreloadSandboxViolations('// hi\nconst x = require("node:os")')).toEqual([
|
||||
{ kind: 'node-builtin', detail: 'node:os' }
|
||||
])
|
||||
})
|
||||
|
||||
// Why: same line on purpose — an unterminated string already stops at a newline, so only a
|
||||
// same-line regex can swallow the request that follows it.
|
||||
it('still flags a real request after a regex holding an unpaired quote', () => {
|
||||
expect(findPreloadSandboxViolations('const q = /["]/; const x = require("node:zlib")')).toEqual(
|
||||
[{ kind: 'node-builtin', detail: 'node:zlib' }]
|
||||
)
|
||||
})
|
||||
|
||||
it('still flags a real request after a division, which is not a regex', () => {
|
||||
expect(findPreloadSandboxViolations('const r = t / 2; const x = require("node:dns")')).toEqual([
|
||||
{ kind: 'node-builtin', detail: 'node:dns' }
|
||||
])
|
||||
})
|
||||
|
||||
it('still flags a real static import that follows a quoted decoy', () => {
|
||||
expect(findPreloadSandboxViolations('const d = "require(\'fs\')";\nimport "node:tty"')).toEqual(
|
||||
[{ kind: 'node-builtin', detail: 'node:tty' }]
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('preload sandbox guard assertions', () => {
|
||||
it('names every offending specifier and the supported surface', () => {
|
||||
expect(() =>
|
||||
|
||||
@@ -63,19 +63,123 @@ function collect(
|
||||
}
|
||||
}
|
||||
|
||||
/** A NUL can never appear in real source, so a masked offset is unambiguous. */
|
||||
const MASKED = '\u0000'
|
||||
|
||||
/**
|
||||
* Marks every comment and string-literal character, preserving offsets, so a match can be tested
|
||||
* for whether it sits in code or in data. Interpolations stay code: `${require('fs')}` is real.
|
||||
*/
|
||||
function maskLexicalSpans(code: string): string {
|
||||
const chars = code.split('')
|
||||
const mask = (from: number, to: number): void => {
|
||||
for (let at = from; at < Math.min(to, chars.length); at += 1) {
|
||||
chars[at] = MASKED
|
||||
}
|
||||
}
|
||||
const interpolations: number[] = [] // brace depth inside each enclosing `${}`
|
||||
let inTemplate = false
|
||||
// A `/` here starts a regex, not a division; anything else makes it an operator.
|
||||
let regexAllowed = true
|
||||
let index = 0
|
||||
while (index < code.length) {
|
||||
const char = code[index]
|
||||
const pair = code.slice(index, index + 2)
|
||||
if (inTemplate) {
|
||||
if (char === '`') {
|
||||
inTemplate = false
|
||||
regexAllowed = false
|
||||
index += 1
|
||||
} else if (pair === '${') {
|
||||
interpolations.push(0)
|
||||
inTemplate = false
|
||||
index += 2
|
||||
} else {
|
||||
const width = char === '\\' ? 2 : 1
|
||||
mask(index, index + width)
|
||||
index += width
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (pair === '//' || pair === '/*') {
|
||||
const close = pair === '//' ? code.indexOf('\n', index) : code.indexOf('*/', index + 2)
|
||||
const end = close === -1 ? code.length : pair === '//' ? close : close + 2
|
||||
mask(index, end)
|
||||
index = end
|
||||
continue
|
||||
}
|
||||
if (char === "'" || char === '"') {
|
||||
let scan = index + 1
|
||||
// A raw newline ends an unterminated literal; nothing after it is still that string.
|
||||
while (scan < code.length && code[scan] !== char && code[scan] !== '\n') {
|
||||
scan += code[scan] === '\\' ? 2 : 1
|
||||
}
|
||||
mask(index, scan + 1)
|
||||
index = scan + 1
|
||||
regexAllowed = false
|
||||
continue
|
||||
}
|
||||
// Why: an unpaired quote inside a regex would otherwise open a string over real code.
|
||||
if (char === '/' && regexAllowed) {
|
||||
let scan = index + 1
|
||||
let inClass = false
|
||||
while (scan < code.length && code[scan] !== '\n' && (inClass || code[scan] !== '/')) {
|
||||
if (code[scan] === '\\') {
|
||||
scan += 1
|
||||
} else if (code[scan] === '[' || code[scan] === ']') {
|
||||
inClass = code[scan] === '['
|
||||
}
|
||||
scan += 1
|
||||
}
|
||||
mask(index, scan + 1)
|
||||
index = scan + 1
|
||||
regexAllowed = false
|
||||
continue
|
||||
}
|
||||
if (!/\s/.test(char)) {
|
||||
regexAllowed = !/[\w$)\]]/.test(char)
|
||||
}
|
||||
if (char === '`') {
|
||||
inTemplate = true
|
||||
} else if (interpolations.length > 0 && (char === '{' || char === '}')) {
|
||||
const last = interpolations.length - 1
|
||||
if (char === '{') {
|
||||
interpolations[last] += 1
|
||||
} else if (interpolations[last] > 0) {
|
||||
interpolations[last] -= 1
|
||||
} else {
|
||||
interpolations.pop()
|
||||
inTemplate = true
|
||||
}
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
return chars.join('')
|
||||
}
|
||||
|
||||
export function findPreloadSandboxViolations(code: string): PreloadSandboxViolation[] {
|
||||
const violations = new Map<string, PreloadSandboxViolation>()
|
||||
// Why: the patterns still run on real source, so specifiers survive; the mask only vetoes
|
||||
// matches whose module keyword is inside a comment or string.
|
||||
const masked = maskLexicalSpans(code)
|
||||
const isData = (match: RegExpExecArray | RegExpMatchArray): boolean => {
|
||||
const keyword = Math.max(match[0].search(/import|export|require/), 0)
|
||||
return masked[(match.index ?? 0) + keyword] === MASKED
|
||||
}
|
||||
for (const pattern of [LITERAL_MODULE_CALL_RE, STATIC_MODULE_SOURCE_RE]) {
|
||||
pattern.lastIndex = 0
|
||||
for (const match of code.matchAll(pattern)) {
|
||||
const specifier = match[2]
|
||||
if (specifier !== undefined) {
|
||||
if (specifier !== undefined && !isData(match)) {
|
||||
collect(violations, classifySpecifier(specifier))
|
||||
}
|
||||
}
|
||||
}
|
||||
NONLITERAL_MODULE_CALL_RE.lastIndex = 0
|
||||
for (const match of code.matchAll(NONLITERAL_MODULE_CALL_RE)) {
|
||||
if (isData(match)) {
|
||||
continue
|
||||
}
|
||||
collect(violations, {
|
||||
kind: 'nonliteral-module-request',
|
||||
detail: code
|
||||
|
||||
Reference in New Issue
Block a user