Skip to content

data-bx-path plugin corrupts regex literals containing <letter — whole file fails to parse #76

Description

@Saurabhwashere

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions