fix(ui): render templates from the path checked against the emails directory
Fixes code scanning alert #327 (js/path-injection).
renderEmailByPath is a server action, so it receives arbitrary strings from the client. It checked them with isPathWithinEmailsDirectory, which resolves a relative path against the emails directory, but then used the original string, which getEmailComponent and fs.readFile resolve against the working directory. A relative path like welcome.tsx passed the check as <emails>/welcome.tsx and was loaded from <cwd>/welcome.tsx.
Our own pages always pass absolute paths, and server actions only accept same-origin calls, so this needed local access to the preview server. The fix makes the checked path the only one that gets used.
Changes
utils/is-path-within-emails-directory.ts→utils/resolve-within-emails-directory.ts:isPathWithinEmailsDirectory(path): booleanbecomesresolveWithinEmailsDirectory(path): string | undefined, returning the real path it checked (orundefinedoutside the emails directory). The check is a singlestartsWith(root + sep)on the real path, the pattern CodeQL recognizes as a guard; an earlier version that also accepted the emails directory itself (=== root || startsWith) kept the alert open. Rendering the directory itself was never meaningful, so it's now rejected too. Otherwise the same rules: traversal, absolute paths, sibling prefixes and escaping symlinks are rejected, and it fails closed without the env var.actions/render-email-by-path.tsx:renderEmailByPathandinvalidateEmailRenderingCacheresolve once and use only the resolved path from then on (rendering, cache keys).actions/get-email-path-from-slug.ts: uses the new function; still returns the absolute candidate path..changeset/ui-resolve-email-path-once.md: patch for@react-email/ui.
Testing
- New tests, written first and failing on
canary:renderEmailByPath('raw-html-email.html')renders the template from the emails directory.renderEmailByPath('vercel-invite-user.tsx')bundles and renders the template from the emails directory (thegetEmailComponentpath CodeQL flagged).
- The guard's spec is ported to the resolver: it now asserts the returned path, plus every existing rejection case, and that the emails directory itself is rejected.
- CodeQL on this PR: no new alerts in changed code, so #327 closes on merge.
- Suites: ui 115/115, react-email 319/319, render 90/90, editor 540/540.
pnpm build,pnpm typecheckand biome pass. - Preview server:
email devonapps/demorenders all 62 templates with HTML identical to before this change, and hot reload (which goes throughinvalidateEmailRenderingCache) still updates the preview.
Summary by cubic
Fixes a path-injection vulnerability in @react-email/ui where a relative template path passed the emails-directory check but was then resolved against the working directory, reading files outside the emails directory.
renderEmailByPath and invalidateEmailRenderingCache resolve the requested path once, against the emails directory, and use only that resolved path for rendering and cache invalidation.
- Replaces
isPathWithinEmailsDirectorywithresolveWithinEmailsDirectory, which returns the real path it validated (orundefinedoutside the directory) so callers can't read a different file. - Adds tests that fail on the previous behavior for raw HTML and bundled
.tsxtemplates.