The javascript: URL problem

React 19 blocks `javascript:` URLs in `href`. React 18 does not — it renders them verbatim, with a console warning at most. Since VivekUI supports `^18 || ^19`, it cannot rely on the framework for this.

The attack is ordinary stored XSS. A CMS field, a user profile link, a webhook payload — anywhere a URL arrives from outside your code and reaches an `href`. If nothing validates the scheme, `javascript:alert(document.cookie)` in that field executes for every visitor who clicks.

Scheme validation, applied everywhere

Every link-rendering component in the library validates the scheme of a consumer-supplied `href` and drops unsafe ones. The exported `isSafeHref` helper is available for your own components too, which matters because the vulnerability lives wherever you handle URLs, not only where the library does.

Links opening in a new tab automatically get `rel="noopener noreferrer"`. Without `noopener`, the opened page can reach back through `window.opener` and navigate the original tab elsewhere — a phishing vector that is trivially avoided and frequently forgotten.

CSV formula injection

This one surprises people. A CSV cell whose value begins with `=`, `+`, `-`, `@`, a tab or a carriage return is interpreted as a formula by Excel and Google Sheets. Formulas can reference external resources and, with a confirmation click, invoke system commands.

The path is entirely realistic: an attacker enters a crafted value into a form field, it is stored, an administrator exports the table to CSV, and it executes on the administrator's machine. The application never rendered anything dangerous — the danger was created by the export.

The library's `toCsv` neutralises those cells. If you write your own CSV export from table data, add the same guard: it is a couple of lines and it closes a genuine hole that most applications have.

No innerHTML anywhere

There is no `dangerouslySetInnerHTML`, no `innerHTML` and no `eval` anywhere in the library. That is a deliberate constraint rather than an accident, and it is what lets the AI chat components guarantee that model output cannot become markup.

It also means there is no escape hatch where a consumer could accidentally introduce one through a prop. A component that accepts an HTML string is a component that will eventually receive an untrusted one.

Storage, globals and module scope

The only `localStorage` access in the entire library is in `ThemeProvider`, wrapped in try/catch because storage throws in some privacy configurations. There is no `console` output, and no `window` or `document` access at module scope.

That last point is what makes the package safe to import in a server environment at all. A library touching `window` when its module is evaluated crashes during server rendering, which is the most common reason a package 'does not work with SSR'.

Supply chain

Packages are published with npm signed provenance, so you can verify a published artefact was built from the repository it claims. CI asserts the published package has no runtime dependencies, and GitHub Actions are pinned to commit SHAs rather than moving tags.

That last one matters more than it sounds: an action referenced by a mutable tag can be changed by whoever controls it, and it runs with access to your build. Pinning to a SHA removes that. Worth applying to your own workflows regardless of which UI library you use.

Key takeaways

  • React 18 renders javascript: URLs verbatim — supporting ^18 means the library must validate schemes itself
  • Every link component validates consumer-supplied hrefs; isSafeHref is exported for your own code
  • target="_blank" gets rel="noopener noreferrer" automatically, closing a window.opener phishing path
  • CSV cells starting = + - @ tab or CR execute in Excel — toCsv neutralises them, and your own exports should too
  • No dangerouslySetInnerHTML, innerHTML or eval anywhere, which is what makes AI chat output safe by construction
  • No window or document at module scope — the usual reason a package breaks under server rendering

Conclusion

Both of these attacks work through data that never looked dangerous: a URL field and a spreadsheet export. A component library is well placed to close them once for everyone, and the CSV guard in particular is worth copying into any application that exports user-supplied data.

Enjoyed this article?

Vivek Kumar Singh

Vivek Kumar Singh

Technical Expert · Full Stack Cloud Engineer · Tokyo, Japan