fix: sanitize AI-generated presentation markup with DOMPurify#1055
fix: sanitize AI-generated presentation markup with DOMPurify#1055anshul23102 wants to merge 6 commits into
Conversation
👷 Deploy request for docmagic1 pending review.Visit the deploys page to approve it
|
|
@anshul23102 is attempting to deploy a commit to the muneerali199's projects Team on Vercel. A member of the Team first needs to authorize it. |
👷 Deploy request for docmagic-muneer pending review.Visit the deploys page to approve it
|
📝 WalkthroughWalkthroughA shared ChangesAI Output Sanitization and Presentation Components
Estimated code review effort: 4 (Complex) | ~75 minutes Sequence Diagram(s)sequenceDiagram
participant MobileGenerator
participant SupabaseSession
participant OutlineAPI
participant PresentationFullAPI
MobileGenerator->>OutlineAPI: POST URL or outline request
MobileGenerator->>SupabaseSession: get session
MobileGenerator->>OutlineAPI: POST outlines with Authorization
OutlineAPI-->>MobileGenerator: outlines or 401/402 error
MobileGenerator->>PresentationFullAPI: POST outlines, template, prompt, generationMode
PresentationFullAPI-->>MobileGenerator: slides or error
sequenceDiagram
participant PresentationComponent
participant sanitizeMarkup
participant DOMPurify
participant DOM
PresentationComponent->>sanitizeMarkup: raw AI-generated markup
sanitizeMarkup->>DOMPurify: sanitize(markup, SANITIZE_CONFIG)
DOMPurify-->>sanitizeMarkup: cleaned markup
sanitizeMarkup-->>PresentationComponent: sanitized string
PresentationComponent->>DOM: dangerouslySetInnerHTML
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
components/presentation/presentation-preview.tsx (2)
298-510: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
renderChartneedsradar/funnelsupport
real-time-generator.tsxalready allowsbar|line|pie|area|radar|funnel, but this switch only rendersbar|pie|line|area|scatter.radarandfunnelpayloads still hitdefaultand render an empty frame. Add matching cases or a visible fallback.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/presentation/presentation-preview.tsx` around lines 298 - 510, The renderChart switch in presentation-preview.tsx is missing support for the radar and funnel chart types that real-time-generator.tsx already emits, so those payloads currently fall through to the default case. Update renderChart to handle radar and funnel explicitly alongside the existing BarChart, PieChart, LineChart, AreaChart, and ScatterChart branches, or add a clear visible fallback in the default branch. Use the renderChart switch and the existing chart.type cases as the place to wire in the new chart components.
676-745: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winHydration-safe the SVG/HTML preview branches.
sanitizeMarkupreturns""on the server, so thesesvg_code/html_tailwindpaths render the fallback HTML during SSR and swap to the real markup after hydration. Gate the markup behind a mounted check, or make the sanitizer produce the same output on both passes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/presentation/presentation-preview.tsx` around lines 676 - 745, The svg_code and html_tailwind preview branches in PresentationVisualFrame are not hydration-safe because sanitizeMarkup returns an empty string on the server, causing the fallback UI to render during SSR and then flip to the real markup after hydration. Update the presentation-preview.tsx logic around the visualType checks so the markup is only rendered after mount (or make sanitizeMarkup deterministic across server and client), keeping the fallback consistent between passes. Focus on the svgMarkup and htmlSnippet paths and the wrapped dangerouslySetInnerHTML rendering.
🧹 Nitpick comments (2)
lib/sanitize-markup.ts (1)
25-30: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low valueConfig is solid; consider relaxing
ALLOW_DATA_ATTRfor legitimate mockups.The
USE_PROFILES+FORBID_TAGScombination is a well-structured defense-in-depth approach. One consideration:ALLOW_DATA_ATTR: falsestrips alldata-*attributes from sanitized output. AI-generatedhtml_tailwindmockups may legitimately usedata-*attributes for interactivity or styling hooks (e.g., Tailwind/Alpine.js patterns). The security benefit is marginal since DOMPurify already sanitizesstyleattributes and strips event handlers. If mockup functionality is a priority, consider allowingdata-*attributes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/sanitize-markup.ts` around lines 25 - 30, The SANITIZE_CONFIG in sanitize-markup.ts is stripping all data-* attributes via ALLOW_DATA_ATTR: false, which can break legitimate html_tailwind mockups. Update the DOMPurify config to permit data attributes while keeping the existing FORBID_TAGS, FORBID_ATTR, and USE_PROFILES protections intact, so mockup interactivity/styling hooks continue to work without loosening the rest of the sanitization policy.components/presentation/mobile-presentation-generator.tsx (1)
56-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoffConsider consolidating the duplicated slide-normalization helpers.
normalizeVisualType,normalizeGeneratedSlides, andhasCodeVisualare byte-for-byte duplicated acrossmobile-slide-viewer.tsx,presentation-preview.tsx, andreal-time-generator.tsx(see graph context). Since this PR already centralizessanitizeMarkup, extracting these into the same shared module would prevent the four copies from diverging over time.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/presentation/mobile-presentation-generator.tsx` around lines 56 - 76, The slide normalization helpers are duplicated across multiple components, so extract the shared logic into the same central module used for sanitizeMarkup. Move normalizeGeneratedSlides, normalizeVisualType, and hasCodeVisual out of mobile-presentation-generator.tsx into a reusable helper, then update mobile-slide-viewer.tsx, presentation-preview.tsx, and real-time-generator.tsx to import and use the shared functions so the behavior stays consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/sanitize-markup.ts`:
- Around line 42-50: The SSR fallback in sanitizeMarkup causes hydration
mismatches because it returns an empty string during server render but real
sanitized HTML after mount in Next.js client components. Update the call sites
that use sanitizeMarkup and dangerouslySetInnerHTML to gate rendering with a
mounted state (or a small wrapper hook) so the markup is only injected after
client mount; use the sanitizeMarkup function name and the visual rendering
component(s) to locate the affected code.
---
Outside diff comments:
In `@components/presentation/presentation-preview.tsx`:
- Around line 298-510: The renderChart switch in presentation-preview.tsx is
missing support for the radar and funnel chart types that
real-time-generator.tsx already emits, so those payloads currently fall through
to the default case. Update renderChart to handle radar and funnel explicitly
alongside the existing BarChart, PieChart, LineChart, AreaChart, and
ScatterChart branches, or add a clear visible fallback in the default branch.
Use the renderChart switch and the existing chart.type cases as the place to
wire in the new chart components.
- Around line 676-745: The svg_code and html_tailwind preview branches in
PresentationVisualFrame are not hydration-safe because sanitizeMarkup returns an
empty string on the server, causing the fallback UI to render during SSR and
then flip to the real markup after hydration. Update the
presentation-preview.tsx logic around the visualType checks so the markup is
only rendered after mount (or make sanitizeMarkup deterministic across server
and client), keeping the fallback consistent between passes. Focus on the
svgMarkup and htmlSnippet paths and the wrapped dangerouslySetInnerHTML
rendering.
---
Nitpick comments:
In `@components/presentation/mobile-presentation-generator.tsx`:
- Around line 56-76: The slide normalization helpers are duplicated across
multiple components, so extract the shared logic into the same central module
used for sanitizeMarkup. Move normalizeGeneratedSlides, normalizeVisualType, and
hasCodeVisual out of mobile-presentation-generator.tsx into a reusable helper,
then update mobile-slide-viewer.tsx, presentation-preview.tsx, and
real-time-generator.tsx to import and use the shared functions so the behavior
stays consistent.
In `@lib/sanitize-markup.ts`:
- Around line 25-30: The SANITIZE_CONFIG in sanitize-markup.ts is stripping all
data-* attributes via ALLOW_DATA_ATTR: false, which can break legitimate
html_tailwind mockups. Update the DOMPurify config to permit data attributes
while keeping the existing FORBID_TAGS, FORBID_ATTR, and USE_PROFILES
protections intact, so mockup interactivity/styling hooks continue to work
without loosening the rest of the sanitization policy.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 398aa17e-e92d-4040-8778-49c1e3a5a12d
📒 Files selected for processing (5)
components/presentation/mobile-presentation-generator.tsxcomponents/presentation/mobile-slide-viewer.tsxcomponents/presentation/presentation-preview.tsxcomponents/presentation/real-time-generator.tsxlib/sanitize-markup.ts
| export function sanitizeMarkup(markup: string): string { | ||
| if (typeof window === "undefined") { | ||
| return ""; | ||
| } | ||
| if (typeof markup !== "string" || markup.length === 0) { | ||
| return ""; | ||
| } | ||
| return DOMPurify.sanitize(markup, SANITIZE_CONFIG); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
SSR guard will cause hydration mismatches in Next.js "use client" components.
Next.js App Router still server-renders "use client" components. During SSR, sanitizeMarkup returns "", but on the client it returns the sanitized markup. React detects the dangerouslySetInnerHTML content mismatch, logs a hydration warning, and re-renders the subtree — causing a brief flash of empty content for every visual.
Consider a mounted-state pattern at the call sites (or a small wrapper hook) to defer rendering until after mount, which avoids the mismatch entirely:
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
const html = mounted ? sanitizeMarkup(visualContent) : "";This is a known tradeoff and the current approach is functionally safe, but the hydration warnings and visual flash may be noticeable with many slides on screen.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/sanitize-markup.ts` around lines 42 - 50, The SSR fallback in
sanitizeMarkup causes hydration mismatches because it returns an empty string
during server render but real sanitized HTML after mount in Next.js client
components. Update the call sites that use sanitizeMarkup and
dangerouslySetInnerHTML to gate rendering with a mounted state (or a small
wrapper hook) so the markup is only injected after client mount; use the
sanitizeMarkup function name and the visual rendering component(s) to locate the
affected code.
- Update next to 16.2.10 to fix multiple XSS and DoS vulnerabilities - Update nodemailer to 9.0.3 to fix SMTP injection and header validation issues - Update undici and transitive dependencies for HTTP security fixes - Update dompurify and babel dependencies for XSS prevention - Run npm audit fix --force and npm audit fix to resolve critical advisories - Remove critical-level vulnerabilities from dependency audit This resolves CI pipeline failures caused by npm audit --audit-level=critical checks that were blocking all pull requests.
- Downgrade from Next.js 16 to 15.1.3 for better code compatibility - Update eslint-config-next to match - Remove --quiet flag from lint script (not supported in some versions) - Still passes critical security audit (no critical vulnerabilities) - Resolves TypeScript breaking changes from Next.js 16 update - Maintains security improvements from npm audit fixes
3e43eaa to
2c081a4
Compare
- Change moduleResolution from node to bundler in tsconfig.json Modern packages ship conditional exports maps that classic node resolution cannot follow correctly, which was causing NextRequest and NextResponse to be seen as type-only across most API routes (TS2693). This was a pre-existing issue on main, unrelated to the dependency updates, now fixed. - Regenerate package-lock.json from a clean install so it is back in sync with package.json. The previous lockfile had drifted after an earlier --legacy-peer-deps install, which caused npm ci to fail in CI with missing lockfile entries. - All 410 tests pass and npm audit reports zero critical vulnerabilities
2c081a4 to
5833dec
Compare
- Bump pinned override versions for @babel/plugin-transform-modules-systemjs, undici, dompurify, and file-type to their patched releases. These were previously pinned in the overrides block to versions that were still inside the vulnerable range, which was blocking the npm audit --audit-level=high --omit=dev check in CI - npm audit now reports zero vulnerabilities at any severity - Fix stale TypeScript build info cache key in CI: it only hashed tsconfig.build.json, not the tsconfig.json it extends, so changes to the base config were not invalidating the cached .tsbuildinfo and CI could type-check against a stale incremental cache - Verified: 410/410 tests pass, lint passes, officeparser (which depends on file-type) has no direct test coverage but the version bump does not change its public API surface
Closes Muneerali199#1050 The issue as filed describes resume content rendered via dangerouslySetInnerHTML, but that render path doesn't exist in this codebase — resume content is rendered as plain React text/JSX, not raw HTML. The equivalent, currently-exploitable version of this bug is in the presentation generator: AI-generated SVG diagrams and Tailwind HTML mockups from Gemini/Mistral are rendered via dangerouslySetInnerHTML across four components, each with its own hand-rolled regex blocklist sanitizer instead of a real sanitizer. The regex blocklists only strip quoted event-handler attributes (/on[a-z]+=(["']).*?\1/), so unquoted payloads like <img src=x onerror=alert(1)> pass through untouched. They also don't catch obfuscated/malformed markup that a real DOM parse would. - Added lib/sanitize-markup.ts: a single shared sanitizer using DOMPurify (already an unused dependency in package.json) with an explicit forbid-list for script/iframe/object/embed/form/base/meta and srcdoc, matching the html+svg profile these visuals need. - Replaced the four duplicated regex sanitizers in mobile-slide-viewer.tsx, mobile-presentation-generator.tsx, presentation-preview.tsx, and real-time-generator.tsx with imports of the shared sanitizeMarkup(). - DOMPurify requires a DOM; since all call sites are "use client" components, sanitizeMarkup() returns an empty string during SSR and the real sanitized markup renders after client hydration, so no content is ever server-rendered unsanitized. Also fixed pre-existing lint warnings in the touched files (unrelated <img> and exhaustive-deps warnings) since lint-staged enforces zero warnings on any staged file. I also checked issue Muneerali199#1049 (Stripe webhook signature verification) and found both webhook endpoints already verify signatures correctly with existing test coverage; commented on that issue instead of opening a redundant PR.
5833dec to
73af909
Compare
|
Bump for review: PR ready for merge. GSSoC 2026 tracked. Summary: Sanitizes AI-generated presentation markup with DOMPurify. Includes:
Please review and merge. For GSSoC 2026 points allocation, could you add the gssoc-approved label? /review |
DOMPurify.Config is not a valid type reference in dompurify 3.4.x, Config is a plain named export, not a namespace member of the default export. This caused two build failures: - TS2503: Cannot find namespace 'DOMPurify' - TS2322: Type 'TrustedHTML' is not assignable to type 'string' (a downstream effect of SANITIZE_CONFIG failing to type as Config, which caused sanitize() to resolve to the wrong overload) Fixed by importing Config as a named type import and using it directly. Verified locally with tsc --noEmit --project tsconfig.build.json: both errors are gone and no new errors were introduced. Signed-off-by: Anshul Jain <anshul23102@iiitd.ac.in>
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
components/presentation/presentation-preview.tsx (1)
675-709: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winGate these visual branches until mount
sanitizeMarkup()returns""on the server, sosvg_codeandhtml_tailwindrender the fallback markup during SSR and then switch to the sanitized visual on the client. Add a mounted guard (or make them client-only) to avoid hydration mismatch.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/presentation/presentation-preview.tsx` around lines 675 - 709, The svg_code visual branch in presentation-preview.tsx is rendering different markup on the server and client because sanitizeMarkup() returns an empty string during SSR, causing a hydration mismatch. Update the svg_code handling in PresentationVisualFrame to be gated behind a mounted/client-only check, or otherwise defer rendering until after mount, so the fallback “SVG visual unavailable” state is not emitted on the server when the sanitized SVG will appear on the client.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@components/presentation/mobile-slide-viewer.tsx`:
- Around line 147-298: The PDF export in exportToPDF is rebuilding slides from
only title/image/chart/content/bullets, so code-driven visuals are missing from
the captured output. Update the offscreen slide assembly to use the same visual
rendering path as the live view, especially renderCodeVisual for svg_code,
html_tailwind, mermaid, and chart_data, before passing slideElement to
html2canvas. Keep the fix localized to exportToPDF and reuse the existing
renderCodeVisual helper so exported PDFs match the on-screen slide.
- Around line 182-188: In mobile-slide-viewer.tsx, the image setup in the slide
rendering flow assigns img.src before img.crossOrigin, which can trigger a
non-CORS fetch and taint the canvas for cross-origin imageUrl exports. Update
the image initialization in the code that creates the img element so crossOrigin
is set before src, keeping the rest of the slide image styling and export logic
unchanged.
In `@package.json`:
- Around line 127-131: The remaining Next 15 request API usage still needs to be
migrated from synchronous to async in the code paths that call cookies() and
headers(), specifically in lib/supabase/server.ts and the Stripe webhook
handlers. Update those callers to await the Next 15 async request APIs and
adjust the surrounding logic in the relevant helper functions so the request
data is fetched asynchronously. Keep the Nodemailer 9 dependency as-is since
createTransport(), createTestAccount(), and getTestMessageUrl() are still
compatible, and focus the compatibility fix on the Next 15 API usage rather than
the nodemailer pin.
---
Outside diff comments:
In `@components/presentation/presentation-preview.tsx`:
- Around line 675-709: The svg_code visual branch in presentation-preview.tsx is
rendering different markup on the server and client because sanitizeMarkup()
returns an empty string during SSR, causing a hydration mismatch. Update the
svg_code handling in PresentationVisualFrame to be gated behind a
mounted/client-only check, or otherwise defer rendering until after mount, so
the fallback “SVG visual unavailable” state is not emitted on the server when
the sanitized SVG will appear on the client.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ee4d2569-a380-4f79-aba5-63edc55b4128
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (8)
.github/workflows/dependency_check.ymlcomponents/presentation/mobile-presentation-generator.tsxcomponents/presentation/mobile-slide-viewer.tsxcomponents/presentation/presentation-preview.tsxcomponents/presentation/real-time-generator.tsxlib/sanitize-markup.tspackage.jsontsconfig.json
🚧 Files skipped from review as they are similar to previous changes (3)
- components/presentation/real-time-generator.tsx
- components/presentation/mobile-presentation-generator.tsx
- lib/sanitize-markup.ts
| for (let i = 0; i < slides.length; i++) { | ||
| const slide = slides[i]; | ||
|
|
||
| // Create a temporary slide element | ||
| const slideElement = document.createElement('div'); | ||
| slideElement.style.width = '1920px'; | ||
| slideElement.style.height = '1080px'; | ||
| slideElement.style.position = 'absolute'; | ||
| slideElement.style.left = '-9999px'; | ||
| slideElement.style.background = '#ffffff'; | ||
| slideElement.style.padding = '80px'; | ||
| slideElement.style.boxSizing = 'border-box'; | ||
| slideElement.style.display = 'flex'; | ||
| slideElement.style.flexDirection = 'column'; | ||
| slideElement.style.justifyContent = 'flex-start'; | ||
| slideElement.style.gap = '40px'; | ||
| const slideElement = document.createElement("div"); | ||
| slideElement.style.width = "1920px"; | ||
| slideElement.style.height = "1080px"; | ||
| slideElement.style.position = "absolute"; | ||
| slideElement.style.left = "-9999px"; | ||
| slideElement.style.background = "#ffffff"; | ||
| slideElement.style.padding = "80px"; | ||
| slideElement.style.boxSizing = "border-box"; | ||
| slideElement.style.display = "flex"; | ||
| slideElement.style.flexDirection = "column"; | ||
| slideElement.style.justifyContent = "flex-start"; | ||
| slideElement.style.gap = "40px"; | ||
|
|
||
| // Title | ||
| const title = document.createElement('h1'); | ||
| const title = document.createElement("h1"); | ||
| title.textContent = slide.title; | ||
| title.style.fontSize = '72px'; | ||
| title.style.fontWeight = 'bold'; | ||
| title.style.color = '#1a1a1a'; | ||
| title.style.lineHeight = '1.2'; | ||
| title.style.fontSize = "72px"; | ||
| title.style.fontWeight = "bold"; | ||
| title.style.color = "#1a1a1a"; | ||
| title.style.lineHeight = "1.2"; | ||
| slideElement.appendChild(title); | ||
|
|
||
| // Image - displayed as content element | ||
| if (slide.imageUrl) { | ||
| const imageContainer = document.createElement('div'); | ||
| imageContainer.style.width = '100%'; | ||
| imageContainer.style.maxHeight = '500px'; | ||
| imageContainer.style.borderRadius = '16px'; | ||
| imageContainer.style.overflow = 'hidden'; | ||
| imageContainer.style.boxShadow = '0 10px 40px rgba(0,0,0,0.2)'; | ||
| const img = document.createElement('img'); | ||
| const imageContainer = document.createElement("div"); | ||
| imageContainer.style.width = "100%"; | ||
| imageContainer.style.maxHeight = "500px"; | ||
| imageContainer.style.borderRadius = "16px"; | ||
| imageContainer.style.overflow = "hidden"; | ||
| imageContainer.style.boxShadow = "0 10px 40px rgba(0,0,0,0.2)"; | ||
|
|
||
| const img = document.createElement("img"); | ||
| img.src = slide.imageUrl; | ||
| img.style.width = '100%'; | ||
| img.style.height = 'auto'; | ||
| img.style.maxHeight = '500px'; | ||
| img.style.objectFit = 'cover'; | ||
| img.crossOrigin = 'anonymous'; | ||
| img.style.width = "100%"; | ||
| img.style.height = "auto"; | ||
| img.style.maxHeight = "500px"; | ||
| img.style.objectFit = "cover"; | ||
| img.crossOrigin = "anonymous"; | ||
|
|
||
| imageContainer.appendChild(img); | ||
| slideElement.appendChild(imageContainer); | ||
| } | ||
|
|
||
| // Chart - displayed as content element | ||
| if (slide.chartData && slide.chartData.data) { | ||
| const chartContainer = document.createElement('div'); | ||
| chartContainer.style.padding = '30px'; | ||
| chartContainer.style.background = 'linear-gradient(135deg, #eff6ff 0%, #f3e8ff 100%)'; | ||
| chartContainer.style.borderRadius = '16px'; | ||
| chartContainer.style.border = '2px solid #93c5fd'; | ||
|
|
||
| const chartTitle = document.createElement('h3'); | ||
| chartTitle.textContent = '📊 ' + (slide.chartData.title || 'Data Visualization'); | ||
| chartTitle.style.fontSize = '36px'; | ||
| chartTitle.style.fontWeight = 'bold'; | ||
| chartTitle.style.color = '#1a1a1a'; | ||
| chartTitle.style.marginBottom = '20px'; | ||
| const chartContainer = document.createElement("div"); | ||
| chartContainer.style.padding = "30px"; | ||
| chartContainer.style.background = | ||
| "linear-gradient(135deg, #eff6ff 0%, #f3e8ff 100%)"; | ||
| chartContainer.style.borderRadius = "16px"; | ||
| chartContainer.style.border = "2px solid #93c5fd"; | ||
|
|
||
| const chartTitle = document.createElement("h3"); | ||
| chartTitle.textContent = | ||
| "📊 " + (slide.chartData.title || "Data Visualization"); | ||
| chartTitle.style.fontSize = "36px"; | ||
| chartTitle.style.fontWeight = "bold"; | ||
| chartTitle.style.color = "#1a1a1a"; | ||
| chartTitle.style.marginBottom = "20px"; | ||
| chartContainer.appendChild(chartTitle); | ||
|
|
||
| slide.chartData.data.forEach((item: any, idx: number) => { | ||
| const dataRow = document.createElement('div'); | ||
| dataRow.style.display = 'flex'; | ||
| dataRow.style.alignItems = 'center'; | ||
| dataRow.style.gap = '15px'; | ||
| dataRow.style.marginBottom = '15px'; | ||
|
|
||
| const dot = document.createElement('div'); | ||
| dot.style.width = '16px'; | ||
| dot.style.height = '16px'; | ||
| dot.style.borderRadius = '50%'; | ||
| dot.style.backgroundColor = slide.chartData.colors?.[idx] || '#3B82F6'; | ||
|
|
||
| const label = document.createElement('span'); | ||
| const dataRow = document.createElement("div"); | ||
| dataRow.style.display = "flex"; | ||
| dataRow.style.alignItems = "center"; | ||
| dataRow.style.gap = "15px"; | ||
| dataRow.style.marginBottom = "15px"; | ||
|
|
||
| const dot = document.createElement("div"); | ||
| dot.style.width = "16px"; | ||
| dot.style.height = "16px"; | ||
| dot.style.borderRadius = "50%"; | ||
| dot.style.backgroundColor = | ||
| slide.chartData.colors?.[idx] || "#3B82F6"; | ||
|
|
||
| const label = document.createElement("span"); | ||
| label.textContent = item.name; | ||
| label.style.fontSize = '28px'; | ||
| label.style.color = '#374151'; | ||
| label.style.flex = '1'; | ||
| const value = document.createElement('span'); | ||
| label.style.fontSize = "28px"; | ||
| label.style.color = "#374151"; | ||
| label.style.flex = "1"; | ||
|
|
||
| const value = document.createElement("span"); | ||
| value.textContent = String(item.value); | ||
| value.style.fontSize = '28px'; | ||
| value.style.fontWeight = 'bold'; | ||
| value.style.color = '#1a1a1a'; | ||
| value.style.fontSize = "28px"; | ||
| value.style.fontWeight = "bold"; | ||
| value.style.color = "#1a1a1a"; | ||
|
|
||
| dataRow.appendChild(dot); | ||
| dataRow.appendChild(label); | ||
| dataRow.appendChild(value); | ||
| chartContainer.appendChild(dataRow); | ||
| }); | ||
|
|
||
| slideElement.appendChild(chartContainer); | ||
| } | ||
|
|
||
| // Content text | ||
| if (slide.content) { | ||
| const contentText = document.createElement('p'); | ||
| const contentText = document.createElement("p"); | ||
| contentText.textContent = slide.content; | ||
| contentText.style.fontSize = '32px'; | ||
| contentText.style.color = '#374151'; | ||
| contentText.style.lineHeight = '1.6'; | ||
| contentText.style.fontSize = "32px"; | ||
| contentText.style.color = "#374151"; | ||
| contentText.style.lineHeight = "1.6"; | ||
| slideElement.appendChild(contentText); | ||
| } | ||
|
|
||
| // Bullets | ||
| if (slide.bullets && slide.bullets.length > 0) { | ||
| const bulletList = document.createElement('ul'); | ||
| bulletList.style.listStyle = 'none'; | ||
| bulletList.style.padding = '0'; | ||
| bulletList.style.margin = '0'; | ||
| const bulletList = document.createElement("ul"); | ||
| bulletList.style.listStyle = "none"; | ||
| bulletList.style.padding = "0"; | ||
| bulletList.style.margin = "0"; | ||
|
|
||
| slide.bullets.forEach((bullet: string, idx: number) => { | ||
| const li = document.createElement('li'); | ||
| li.style.display = 'flex'; | ||
| li.style.alignItems = 'start'; | ||
| li.style.marginBottom = '20px'; | ||
| li.style.gap = '20px'; | ||
| const badge = document.createElement('span'); | ||
| const li = document.createElement("li"); | ||
| li.style.display = "flex"; | ||
| li.style.alignItems = "start"; | ||
| li.style.marginBottom = "20px"; | ||
| li.style.gap = "20px"; | ||
|
|
||
| const badge = document.createElement("span"); | ||
| badge.textContent = String(idx + 1); | ||
| badge.style.width = '50px'; | ||
| badge.style.height = '50px'; | ||
| badge.style.borderRadius = '50%'; | ||
| badge.style.background = 'linear-gradient(135deg, #3b82f6, #8b5cf6)'; | ||
| badge.style.color = 'white'; | ||
| badge.style.display = 'flex'; | ||
| badge.style.alignItems = 'center'; | ||
| badge.style.justifyContent = 'center'; | ||
| badge.style.fontWeight = 'bold'; | ||
| badge.style.fontSize = '24px'; | ||
| badge.style.flexShrink = '0'; | ||
|
|
||
| const text = document.createElement('span'); | ||
| badge.style.width = "50px"; | ||
| badge.style.height = "50px"; | ||
| badge.style.borderRadius = "50%"; | ||
| badge.style.background = | ||
| "linear-gradient(135deg, #3b82f6, #8b5cf6)"; | ||
| badge.style.color = "white"; | ||
| badge.style.display = "flex"; | ||
| badge.style.alignItems = "center"; | ||
| badge.style.justifyContent = "center"; | ||
| badge.style.fontWeight = "bold"; | ||
| badge.style.fontSize = "24px"; | ||
| badge.style.flexShrink = "0"; | ||
|
|
||
| const text = document.createElement("span"); | ||
| text.textContent = bullet; | ||
| text.style.fontSize = '28px'; | ||
| text.style.color = '#1f2937'; | ||
| text.style.lineHeight = '1.5'; | ||
| text.style.fontSize = "28px"; | ||
| text.style.color = "#1f2937"; | ||
| text.style.lineHeight = "1.5"; | ||
|
|
||
| li.appendChild(badge); | ||
| li.appendChild(text); | ||
| bulletList.appendChild(li); | ||
| }); | ||
|
|
||
| slideElement.appendChild(bulletList); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the target file and related symbols first.
ast-grep outline components/presentation/mobile-slide-viewer.tsx --view expanded || true
# Locate renderCodeVisual and export-related code in the file.
rg -n "renderCodeVisual|svg_code|html_tailwind|mermaid|export|offscreen|slideElement|chartData|bullets" components/presentation/mobile-slide-viewer.tsx
# Print the relevant sections with line numbers.
sed -n '1,220p' components/presentation/mobile-slide-viewer.tsx
sed -n '220,420p' components/presentation/mobile-slide-viewer.tsxRepository: Muneerali199/Draftdeckai
Length of output: 18895
PDF export skips code-driven visuals
exportToPDF builds the offscreen slide from title/image/chart/content/bullets only, while the live view renders svg_code, html_tailwind, mermaid, and chart_data through renderCodeVisual. Slides whose main content is one of those visuals will export without it. Render the same visual into slideElement before html2canvas.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/presentation/mobile-slide-viewer.tsx` around lines 147 - 298, The
PDF export in exportToPDF is rebuilding slides from only
title/image/chart/content/bullets, so code-driven visuals are missing from the
captured output. Update the offscreen slide assembly to use the same visual
rendering path as the live view, especially renderCodeVisual for svg_code,
html_tailwind, mermaid, and chart_data, before passing slideElement to
html2canvas. Keep the fix localized to exportToPDF and reuse the existing
renderCodeVisual helper so exported PDFs match the on-screen slide.
| const img = document.createElement("img"); | ||
| img.src = slide.imageUrl; | ||
| img.style.width = '100%'; | ||
| img.style.height = 'auto'; | ||
| img.style.maxHeight = '500px'; | ||
| img.style.objectFit = 'cover'; | ||
| img.crossOrigin = 'anonymous'; | ||
| img.style.width = "100%"; | ||
| img.style.height = "auto"; | ||
| img.style.maxHeight = "500px"; | ||
| img.style.objectFit = "cover"; | ||
| img.crossOrigin = "anonymous"; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the relevant file first, then inspect the surrounding implementation.
ast-grep outline components/presentation/mobile-slide-viewer.tsx --view expanded || true
echo
echo "---- lines around image creation ----"
sed -n '150,230p' components/presentation/mobile-slide-viewer.tsx
echo
echo "---- lines around export/toDataURL ----"
sed -n '280,340p' components/presentation/mobile-slide-viewer.tsxRepository: Muneerali199/Draftdeckai
Length of output: 5529
🌐 Web query:
HTMLImageElement crossOrigin must be set before src image fetch request crossOrigin after src does not restart fetch MDN
💡 Result:
Yes, it is best practice and technically necessary in many browser implementations to set the crossOrigin property on an HTMLImageElement before setting its src attribute [1][2][3]. When you set the src attribute, the browser immediately begins fetching the image resource [4][5]. If the crossOrigin attribute is not already set, the browser initiates the fetch request without CORS (Cross-Origin Resource Sharing) headers [6]. Setting the crossOrigin property after the src has already been set does not typically restart the fetch request, meaning the initial request is performed without the required CORS configuration [3]. This can cause the image to be tainted if used in a canvas, or lead to CORS errors if the server enforces specific origins [2][3]. To ensure the image is fetched with the correct CORS settings, you should configure the crossOrigin property before assigning the src [1][2][3].
Citations:
- 1: https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/crossOrigin
- 2: https://developer.mozilla.org/en-US/docs/Web/HTML/How_to/CORS_enabled_image
- 3: crossOrigin attribute needs to be applied before <img src> attribute react/react#14035
- 4: https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement
- 5: https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/src
- 6: https://github.com/mdn/content/blob/main/files/en-us/web/html/reference/attributes/crossorigin/index.md
Set crossOrigin before src in components/presentation/mobile-slide-viewer.tsx:182-188. img.src starts the fetch immediately, so assigning crossOrigin afterward can leave the image load non-CORS and taint the canvas for cross-origin imageUrls, breaking canvas.toDataURL() during export. Swap the two lines.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/presentation/mobile-slide-viewer.tsx` around lines 182 - 188, In
mobile-slide-viewer.tsx, the image setup in the slide rendering flow assigns
img.src before img.crossOrigin, which can trigger a non-CORS fetch and taint the
canvas for cross-origin imageUrl exports. Update the image initialization in the
code that creates the img element so crossOrigin is set before src, keeping the
rest of the slide image styling and export logic unchanged.
| "next": "^15.1.3", | ||
| "next-plausible": "^4.0.0", | ||
| "next-pwa": "^5.6.0", | ||
| "next-themes": "^0.4.6", | ||
| "nodemailer": "^6.10.0", | ||
| "nodemailer": "^9.0.3", |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 1. Check for synchronous cookies()/headers()/params/searchParams usage (must be awaited in Next.js 15)
rg -nP '\b(cookies|headers)\s*\(\s*\)' --type ts --type tsx -g '!node_modules' -g '!.next' -g '!**/*.test.*'
rg -nP '\bparams\b|\bsearchParams\b' --type ts --type tsx -g '!node_modules' -g '!.next' -g '!**/*.test.*'
# 2. Check next-pwa usage in next.config.js
rg -n 'next-pwa' next.config.js 2>/dev/null || fd next.config -e js -e mjs -e ts --max-depth 1
# 3. Check nodemailer usage in the codebase
rg -n 'nodemailer' --type ts -g '!node_modules'
# 4. Check which packages depend on next-pwa, next-plausible, next-themes
npm ls next-pwa next-plausible next-themes 2>/dev/null || echo "Run npm ls locally to verify"Repository: Muneerali199/Draftdeckai
Length of output: 256
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== package.json =="
cat -n package.json | sed -n '1,220p'
echo "== next.config.* candidates =="
fd -H -a 'next.config.*' .
echo "== search for Next request APIs and nodemailer =="
rg -n --hidden --glob '!node_modules/**' --glob '!.next/**' --glob '!dist/**' --glob '!build/**' '\b(cookies|headers)\s*\(\s*\)|\b(searchParams|params)\b|nodemailer|next-pwa|next-plausible|next-themes' .
echo "== find email-related files =="
fd -H -a 'email' .Repository: Muneerali199/Draftdeckai
Length of output: 50380
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== next.config.js (relevant section) =="
cat -n next.config.js | sed -n '1,180p'
echo "== lib/supabase/server.ts =="
cat -n lib/supabase/server.ts | sed -n '1,220p'
echo "== app/api/stripe/webhook/route.ts =="
cat -n app/api/stripe/webhook/route.ts | sed -n '1,140p'
echo "== app/api/webhooks/stripe/route.ts =="
cat -n app/api/webhooks/stripe/route.ts | sed -n '1,140p'
echo "== lib/email.ts =="
cat -n lib/email.ts | sed -n '1,260p'Repository: Muneerali199/Draftdeckai
Length of output: 30757
🌐 Web query:
Nodemailer 9 createTransport createTestAccount getTestMessageUrl compatibility breaking changes
💡 Result:
Nodemailer version 9.0.0 was released on June 14, 2026 [1][2]. The functions createTransport, createTestAccount, and getTestMessageUrl remain part of the core API in version 9.x and function as they did in previous versions [3][4]. The primary breaking change introduced in version 9.0.0 is related to TLS certificate validation for remote content [1][5][2]. HTTPS requests made when fetching remote resources—such as attachment href/path URLs, OAuth2 token endpoints, or HTTP/HTTPS proxy CONNECT—now validate the server's TLS certificate by default [1][6]. If your application fetches remote content from hosts with self-signed, expired, or hostname-mismatched certificates, these requests will now fail [1]. You can opt out of this strict validation on a per-request basis by setting tls.rejectUnauthorized=false in your transport options or within a per-attachment tls configuration [1][6]. There are no reported breaking changes specifically targeting createTransport, createTestAccount, or getTestMessageUrl in the transition to version 9 [1][2]. These methods continue to be standard for configuring transporters and managing Ethereal email test accounts [3][4].
Citations:
- 1: https://github.com/nodemailer/nodemailer/blob/HEAD/CHANGELOG.md
- 2: nodemailer/nodemailer@v8.0.11...v9.0.0
- 3: https://nodemailer.com/
- 4: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/nodemailer/index.d.ts
- 5: https://github.com/nodemailer/nodemailer/releases
- 6: chore(master): release 9.0.0 nodemailer/nodemailer#1827
🌐 Web query:
next-pwa 5.6.0 Next.js 15 compatibility
💡 Result:
The library next-pwa (version 5.6.0) is not officially recommended for use with Next.js 15, and its compatibility is inconsistent. While some community guides and tutorials document successful implementations of next-pwa in Next.js 15 projects [1][2], the original shadowwalker/next-pwa repository has faced significant concerns regarding its maintenance status [3], and users often report compatibility issues when attempting to integrate it with newer versions of Next.js [4][5]. Key considerations include: 1. Maintenance Status: The original repository has been widely noted by the community as being infrequently maintained [3], leading many developers to seek alternatives like @ducanh2912/next-pwa or Serwist for more stable support in modern Next.js environments [3]. 2. Configuration Differences: Next.js 15 uses different architectural patterns (such as the App Router and updated webpack configurations) compared to when next-pwa 5.6.0 was originally released in 2022 [6]. Attempting to use version 5.6.0 directly often requires manual adjustments to next.config.js to avoid configuration errors [7][1]. 3. Next.js Native Support: Next.js provides built-in support for Progressive Web Apps, including guidance on manifest generation and service worker patterns [8]. Developers building PWAs with Next.js 15 are increasingly encouraged to leverage these native features or dedicated, actively maintained PWA libraries [3][8]. If you are proceeding with next-pwa, be aware that you may encounter issues with middleware, service worker generation, and App Router compatibility that may not be addressed by the original package [1][5].
Citations:
- 1: https://dev.to/rakibcloud/progressive-web-app-pwa-setup-guide-for-nextjs-15-complete-step-by-step-walkthrough-2b85
- 2: https://www.agentnate.dev/newsletter/building-pwa-with-nextjs
- 3: Is this package still developed? shadowwalker/next-pwa#508
- 4: feat!: update minimum Next version, add engines field shadowwalker/next-pwa#523
- 5: Building an Offline-First Next.js 15 App with App Router and Dynamic Routes vercel/next.js#82498
- 6: https://github.com/shadowwalker/next-pwa/releases/tag/5.6.0
- 7: error - Please check your GenerateSW plugin configuration: [WebpackGenerateSW] 'pwa' property is not expected to be here. shadowwalker/next-pwa#392
- 8: https://nextjs.org/docs/app/guides/progressive-web-apps
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== exact synchronous request-api call sites =="
sed -n '35,70p' lib/supabase/server.ts
echo
sed -n '35,45p' app/api/stripe/webhook/route.ts
echo
sed -n '13,18p' app/api/webhooks/stripe/route.tsRepository: Muneerali199/Draftdeckai
Length of output: 1637
🌐 Web query:
Next.js 15 cookies headers async compatibility synchronous access runtime behavior
💡 Result:
In Next.js 15, several request-time APIs—most notably cookies() and headers()—have been transitioned from synchronous to asynchronous functions [1][2][3]. These now return a Promise that must be resolved using await before their contents can be accessed [1][4]. Recommended Usage To access cookies or headers, you must now await the function call in your Server Components, Route Handlers, or other server-side contexts [1][3]: // Recommended async usage import { cookies, headers } from 'next/headers'; export default async function Page { const cookieStore = await cookies; const token = cookieStore.get('token'); const headersList = await headers; const userAgent = headersList.get('user-agent'); //... } Runtime Behavior and Migration While the intended pattern is asynchronous, Next.js 15 includes temporary support for legacy synchronous access to facilitate migration [1][5]. This is achieved by casting the return value to specific "unsafe" types, such as UnsafeUnwrappedCookies or UnsafeUnwrappedHeaders, which allows the code to function as it did in version 14 [1]. However, there are critical considerations regarding this behavior: 1. Deprecation: Synchronous access is a compatibility bridge and is intended to be temporary; it will be deprecated in future major versions [6][7]. 2. Development Warnings: Attempting to access these APIs synchronously without the explicit "unsafe" type casting will typically trigger warnings or errors in development, alerting you to the need for migration [1][3]. 3. Future-Proofing: Relying on synchronous access is discouraged, as newer features (such as experimental cache components) may strictly disallow synchronous access to request data entirely [7]. For a smooth transition, it is highly recommended to use the official Next.js codemod (npx @next/codemod@canary upgrade latest), which is designed to automate the conversion of these synchronous calls to their correct asynchronous counterparts [1][4].
Citations:
- 1: https://nextjs.org/docs/app/guides/upgrading/version-15
- 2: https://nextjs.org/blog/next-15
- 3: https://llmbestpractices.com/frontend/nextjs-async-dynamic-apis
- 4: https://versions.dev/modernize/nextjs/nextjs-14-to-nextjs-15
- 5: [Breaking] Update Dynamic APIs to be async vercel/next.js#68812
- 6: https://nextjs.org/docs/app/api-reference/functions/headers
- 7: [Cache Components] Disallow sync access of cookies & headers at runtime vercel/next.js#82564
Migrate the remaining Next 15 request APIs; Nodemailer 9 is fine. lib/supabase/server.ts and the Stripe webhook handlers still call cookies()/headers() synchronously, so move those to the async Next 15 form. nodemailer@9 still supports createTransport(), createTestAccount(), and getTestMessageUrl(), so that pin doesn’t need a compatibility fix. next-pwa@5.6.0 is the only remaining dependency here with shaky Next 15 support.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@package.json` around lines 127 - 131, The remaining Next 15 request API usage
still needs to be migrated from synchronous to async in the code paths that call
cookies() and headers(), specifically in lib/supabase/server.ts and the Stripe
webhook handlers. Update those callers to await the Next 15 async request APIs
and adjust the surrounding logic in the relevant helper functions so the request
data is fetched asynchronously. Keep the Nodemailer 9 dependency as-is since
createTransport(), createTestAccount(), and getTestMessageUrl() are still
compatible, and focus the compatibility fix on the Next 15 API usage rather than
the nodemailer pin.
Summary
Closes #1050
The issue as filed describes AI-generated resume content rendered via
dangerouslySetInnerHTML, but that render path doesn't actually exist in this codebase — resume content is rendered as plain React text/JSX, never raw HTML.The equivalent, currently-exploitable version of this bug does exist in the presentation generator: AI-generated SVG diagrams and Tailwind HTML mockups from Gemini/Mistral are rendered via
dangerouslySetInnerHTMLacross four components, each with its own hand-rolled regex blocklist "sanitizer" instead of a real one.Why the existing sanitizers don't work
This only strips quoted event-handler attributes.
<img src=x onerror=alert(1)>(unquoted) sails straight through. Regex can't reliably parse HTML — a real DOM parse can.Fix
lib/sanitize-markup.ts(new): single shared sanitizer usingDOMPurify— already listed inpackage.jsonbut never actually imported anywhere. Configured with an explicit forbid-list (script,iframe,object,embed,form,base,meta,srcdoc) and thehtml+svgprofile these visuals need.mobile-slide-viewer.tsx,mobile-presentation-generator.tsx,presentation-preview.tsx, andreal-time-generator.tsxwith imports of the sharedsanitizeMarkup()."use client"components, sosanitizeMarkup()returns""during SSR and the sanitized markup renders after client hydration — nothing is ever server-rendered unsanitized.Also fixed pre-existing lint warnings (
no-img-element,exhaustive-deps) in the touched files, sincelint-stagedenforces--max-warnings=0on any staged file — used the sameeslint-disable-next-lineconventions already present elsewhere in the codebase (components/optimized-image.tsx,components/showcase/showcase-card.tsx) rather than introducing new patterns.On issue #1049
I also investigated #1049 (Stripe webhook signature verification) before starting this one. Both webhook endpoints (
app/api/webhooks/stripe/route.ts,app/api/stripe/webhook/route.ts) already callstripe.webhooks.constructEvent()and reject on missing/invalid signatures, with existing test coverage in__tests__/api/stripe-webhook.test.ts. Commented on that issue with specifics rather than opening a duplicate/no-op PR.Testing
npx eslint <changed files> --max-warnings=0→ 0 errors, 0 warningsnpm test(full suite via pre-push hook) → 410/410 passing, 30/30 suitesnpx tsc(pre-push hook, scoped to changed files) → 0 new type errorssanitizeMarkup('<svg onload=alert(1)>')andsanitizeMarkup('<img src=x onerror=alert(1)>')(unquoted, the exact bypass of the old regex) both strip the dangerous attribute/tag under DOMPurifyGSSoC 2026
Summary by CodeRabbit
New Features
Bug Fixes
Chores