Skip to content

Commit 3eb8131

Browse files
committed
fix(audits): match only the real type prop, fail closed on expression types, and scan MDX pages
1 parent 68e5bd1 commit 3eb8131

1 file changed

Lines changed: 14 additions & 8 deletions

File tree

‎scripts/check-source-text.ts‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@ const EXECUTABLE_SCRIPT_TYPES = new Set([
5656
])
5757

5858
const NEXT_SCRIPT_IMPORT = /import\s+(\w+)\s*(?:,\s*\{[^}]*\}\s*)?from\s*['"]next\/script['"]/
59-
const TYPE_ATTRIBUTE =
60-
/\btype\s*=\s*(?:'([^']*)'|"([^"]*)"|\{\s*(?:'([^']*)'|"([^"]*)"|`([^`$]*)`)\s*\})/
59+
/** The start of the element's own `type` prop — not a suffix like `data-type`. */
60+
const TYPE_PROP = /(?:^|\s)type\s*=\s*/
61+
/** A statically known `type` value right after {@link TYPE_PROP}. */
62+
const LITERAL_TYPE_VALUE = /^(?:'([^']*)'|"([^"]*)"|\{\s*(?:'([^']*)'|"([^"]*)"|`([^`$]*)`)\s*\})/
6163

6264
/** The attribute text of the JSX opening tag that starts at `start`, skipping `>` inside braces. */
6365
function openingTagAttributes(source: string, start: number): string {
@@ -71,17 +73,21 @@ function openingTagAttributes(source: string, start: number): string {
7173
return source.slice(start)
7274
}
7375

74-
/** Line numbers of `next/script` elements in `source` whose `type` is not JavaScript. */
76+
/**
77+
* Line numbers of `next/script` elements in `source` whose `type` is not JavaScript. A `type` set
78+
* from an expression fails closed: the audit cannot prove it executable.
79+
*/
7580
function findDataNextScripts(source: string): number[] {
7681
const localName = NEXT_SCRIPT_IMPORT.exec(source)?.[1]
7782
if (!localName) return []
7883
const lines: number[] = []
7984
for (const match of source.matchAll(new RegExp(`<${localName}\\b`, 'g'))) {
8085
const attributes = openingTagAttributes(source, match.index + match[0].length)
81-
const typeMatch = TYPE_ATTRIBUTE.exec(attributes)
82-
if (!typeMatch) continue
83-
const type = typeMatch.slice(1).find((value) => value !== undefined) ?? ''
84-
if (EXECUTABLE_SCRIPT_TYPES.has(type.trim().toLowerCase())) continue
86+
const typeProp = TYPE_PROP.exec(attributes)
87+
if (!typeProp) continue
88+
const literal = LITERAL_TYPE_VALUE.exec(attributes.slice(typeProp.index + typeProp[0].length))
89+
const type = literal?.slice(1).find((value) => value !== undefined)
90+
if (type !== undefined && EXECUTABLE_SCRIPT_TYPES.has(type.trim().toLowerCase())) continue
8591
lines.push(source.slice(0, match.index).split('\n').length)
8692
}
8793
return lines
@@ -110,7 +116,7 @@ for (const file of files) {
110116
if (!(await source.exists())) continue
111117
const bytes = await source.bytes()
112118
if (bytes.includes(0)) nulOffenders.push(file)
113-
if (file.startsWith('apps/') && /\.[jt]sx$/.test(file)) {
119+
if (file.startsWith('apps/') && /\.(?:[jt]sx|mdx)$/.test(file)) {
114120
const text = new TextDecoder().decode(bytes)
115121
if (!text.includes('next/script')) continue
116122
for (const line of findDataNextScripts(text)) dataScriptOffenders.push(`${file}:${line}`)

0 commit comments

Comments
 (0)