Package: browser-metro@1.0.30
File: dist/plugins/data-bx-path.js (injectDataBxPath)
Severity: High — a single innocuous regex in user code makes the whole file unparseable and takes down the entire preview bundle.
Summary
createDataBxPathPlugin() lexically scans .tsx/.jsx source for <Tag sequences and injects data-bx-path / dataSet attributes. The scanner's state machine tracks single quotes, double quotes, template literals, line comments, and block comments (states 0–5) — but not regex literals. A regex literal whose body contains < followed by a tag-start character is treated as code position, and the attribute is injected inside the regex.
Minimal repro
// sanitize.tsx
export function sanitizeMarkdown(markdown: string): string {
return markdown.replace(/<script/gi, '');
}
Run through the plugin:
const { createDataBxPathPlugin } = require('browser-metro');
const plugin = createDataBxPathPlugin();
const out = plugin.transformSource({
src: "export function sanitizeMarkdown(markdown) {\n return markdown.replace(/<script/gi, '');\n}",
filename: '/sanitize.tsx',
});
console.log(out.src);
Output:
export function sanitizeMarkdown(markdown) {
return markdown.replace(/<script data-bx-path="/sanitize.tsx:2:28"/gi, '');
}
The regex literal now contains an unescaped " and stray /, so the file no longer parses. Because the transform runs before Sucrase, the module fails to compile, the dependency-bundle build for the whole app dies, and every screen renders a misleading downstream error (in our product: "Module not found: expo-router" on every screen).
Root cause
In injectDataBxPath the scanner state variable covers:
0=normal, 1=single-quote, 2=double-quote, 3=template, 4=line-comment, 5=block-comment
At a / in state 0 the code only checks for // and /*. A / opening a regex literal stays in state 0, so the subsequent <s of /<script/ hits the JSX branch. isJsxContext then looks back, sees the ( (or the / itself) before the <, classifies it as "anything else → likely JSX", and injects.
Any regex whose body contains < followed by [A-Za-z_] is affected, e.g.:
str.replace(/<script/gi, '') — HTML sanitizers (our production hit, 2026-08-30)
/<iframe[^>]*>/g
/^\s*<div/
/(?<name>x)/ (named groups) is likewise mis-scanned, though for a lowercase group name the injected data- attribute happens to land where it still breaks the file.
Suggested fix
Add a regex-literal state to the scanner. In state 0, when hitting / that is not // or /*, use the standard prev-token heuristic to decide regex vs division (the same approach isJsxContext already uses for <):
- previous non-whitespace char is one of
( , = : [ ! & | ? { } ; + - * % ~ ^ or the > of => → regex literal
- previous token is a keyword (
return, typeof, case, …) → regex literal
- otherwise (identifier, number,
), ]) → division, stay in state 0
While in the regex state, honor backslash escapes and [...] character classes (an unescaped / inside a class does not terminate the literal), exit at the closing /, then skip flag chars. Regex literals cannot span lines, so an unterminated scan can bail out at the line end and fall back to treating the / as division.
For reference, our platform now ships a workaround pass that runs before the plugin and rewrites hazardous literals to new RegExp("<script", "gi") string form (which the scanner does track). Happy to share it.
Environment
browser-metro@1.0.30
- Observed in production 2026-08-30; reproduced deterministically with the snippet above.
Package:
browser-metro@1.0.30File:
dist/plugins/data-bx-path.js(injectDataBxPath)Severity: High — a single innocuous regex in user code makes the whole file unparseable and takes down the entire preview bundle.
Summary
createDataBxPathPlugin()lexically scans.tsx/.jsxsource for<Tagsequences and injectsdata-bx-path/dataSetattributes. The scanner's state machine tracks single quotes, double quotes, template literals, line comments, and block comments (states 0–5) — but not regex literals. A regex literal whose body contains<followed by a tag-start character is treated as code position, and the attribute is injected inside the regex.Minimal repro
Run through the plugin:
Output:
The regex literal now contains an unescaped
"and stray/, so the file no longer parses. Because the transform runs before Sucrase, the module fails to compile, the dependency-bundle build for the whole app dies, and every screen renders a misleading downstream error (in our product: "Module not found: expo-router" on every screen).Root cause
In
injectDataBxPaththe scanner state variable covers:At a
/in state 0 the code only checks for//and/*. A/opening a regex literal stays in state 0, so the subsequent<sof/<script/hits the JSX branch.isJsxContextthen looks back, sees the((or the/itself) before the<, classifies it as "anything else → likely JSX", and injects.Any regex whose body contains
<followed by[A-Za-z_]is affected, e.g.:str.replace(/<script/gi, '')— HTML sanitizers (our production hit, 2026-08-30)/<iframe[^>]*>/g/^\s*<div//(?<name>x)/(named groups) is likewise mis-scanned, though for a lowercase group name the injecteddata-attribute happens to land where it still breaks the file.Suggested fix
Add a regex-literal state to the scanner. In state 0, when hitting
/that is not//or/*, use the standard prev-token heuristic to decide regex vs division (the same approachisJsxContextalready uses for<):( , = : [ ! & | ? { } ; + - * % ~ ^or the>of=>→ regex literalreturn,typeof,case, …) → regex literal),]) → division, stay in state 0While in the regex state, honor backslash escapes and
[...]character classes (an unescaped/inside a class does not terminate the literal), exit at the closing/, then skip flag chars. Regex literals cannot span lines, so an unterminated scan can bail out at the line end and fall back to treating the/as division.For reference, our platform now ships a workaround pass that runs before the plugin and rewrites hazardous literals to
new RegExp("<script", "gi")string form (which the scanner does track). Happy to share it.Environment
browser-metro@1.0.30