Skip to content

[chore]: security dependency update and maintenance#351

Merged
Producdevity merged 15 commits into
stagingfrom
chore/update-packages
May 11, 2026
Merged

[chore]: security dependency update and maintenance#351
Producdevity merged 15 commits into
stagingfrom
chore/update-packages

Conversation

@Producdevity

@Producdevity Producdevity commented May 11, 2026

Copy link
Copy Markdown
Owner

Description

Security-floor dependency update/maintenance

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Refactor
  • Other: security dependency maintenance

How Has This Been Tested?

  • Local build
  • Lint
  • Typecheck
  • Unit tests
  • Manual testing

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have made corresponding changes to the documentation
  • I have checked that all checks (lint, typecheck, test) pass

Notes for reviewers

This PR intentionally stays on the security floor. Major upgrades to next@16, @clerk/nextjs@7, and other latest-major work should be handled in a separate PR.

Summary by CodeRabbit

  • New Features

    • Notifications page now requires/signals authentication
    • API docs viewer loads local OpenAPI content for stable/offline display
  • Bug Fixes

    • Unified and strengthened markdown sanitization and validation
    • Improved CORS origin validation across requests
    • Comment form now surfaces the first markdown validation error
  • Style

    • Listings header shows an inline rotating loading indicator
  • Tests

    • Added/expanded tests for CORS and markdown parsing/sanitization
  • Chores

    • Upgraded/pinned core dependencies; updated production base image and deployment ignore rules

Review Change Stack

@vercel

vercel Bot commented May 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
emuready Ready Ready Preview, Comment May 11, 2026 6:48pm

Request Review

@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Pins and upgrades dependencies, centralizes CORS origin parsing and allowlist checks, unifies markdown sanitization (DOMPurify for browser+Node) with expanded tests, gates notifications by Clerk auth with typed category parsing and logging, updates UI spinners and Swagger embedding, and bumps Docker production base image.

Changes

CORS Security & Dependency Updates

Layer / File(s) Summary
Dependency Version Updates
package.json
Pinned and upgraded runtime and dev dependencies: Next 15.5.18, React 19.1.4, tRPC 11.17.0, React Query 5.100.10, dompurify 3.4.2, jsdom 26.1.0, markdown-it 14.1.1, axios 1.16.0, postcss 8.5.14, and Next tooling pinned to 15.5.18.
CORS Helpers & Tests
src/lib/cors.ts, src/lib/cors.test.ts
Adds getOriginFromUrl(url) returning origin or null, and isAllowedRequestOrigin({allowedOrigins, source}) that validates a source against the allowlist; tests cover valid/invalid inputs and missing source.
Middleware CORS Integration
src/middleware.ts
isValidOrigin now delegates to isAllowedRequestOrigin for both the origin and referer headers, replacing inline parsing/matching logic.

Markdown Sanitization & Security

Layer / File(s) Summary
Markdown Core Sanitization
src/utils/markdown.ts
Replaces mixed sanitization with a unified DOMPurify setup (lazy init using browser window or jsdom via non_webpack_require), defines ALLOWED_TAGS/ALLOWED_ATTR, PURIFY_CONFIG and TEXT_ONLY_CONFIG, adds sanitize helpers, and refactors parseMarkdown, stripMarkdown, hasMarkdownSyntax, and validateMarkdown.
Markdown Tests
src/utils/markdown.test.ts
Adds/extends tests asserting scripts and event handlers are sanitized, blocks javascript: and data: link/image protocols, preserves inline-code formatting, validates stripMarkdown, and includes a performance test (parse under 3000ms).
Markdown Node Runtime Tests
src/utils/markdown.node.test.ts
New Node-runtime tests using JSDOM to assert server-side DOMPurify sanitization (no <script> elements and safe markdown renders).

UI/UX & Authentication Updates

Layer / File(s) Summary
Notifications Authentication & Filtering
src/app/notifications/NotificationsPage.tsx
Gates data fetching/rendering on Clerk useUser() (isLoaded/isSignedIn), adds NotificationsLoading and SignInRequired early returns, types category state as `NotificationCategory
Listings Loading Animation
src/app/v2/listings/components/ListingsHeader.tsx
Switches status block from motion.p to motion.div and replaces static loader with an inline rotating motion.span spinner when isLoading.
API Documentation Embedding
src/app/docs/api/swagger/SwaggerPage.tsx
Imports local mobile-openapi.json and supplies it via configuration.content to ApiReferenceReact, removes the previous URL constant, and sets withDefaultFonts: false.
Docker Production Base Image & Vercel
Dockerfile, .vercelignore
Production stage base image updated to node:22.17-alpine. .vercelignore updated to ensure prisma/sql/*.sql and prisma/sql/**/*.sql are not ignored.
Comment Form Validation
src/components/comments/GenericCommentForm.tsx
Validation failure toast now shows only the first markdown validation error instead of a joined list.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant NotificationsPage
  participant Clerk
  participant QueryHooks
  User->>NotificationsPage: Open notifications page
  NotificationsPage->>Clerk: call useUser()
  Clerk-->>NotificationsPage: isLoaded, isSignedIn
  alt Clerk not loaded
    NotificationsPage->>User: show NotificationsLoading
  else Not signed in
    NotificationsPage->>User: show SignInRequired (SignInButton)
  else Signed in
    NotificationsPage->>QueryHooks: enable queries (selected category)
    QueryHooks-->>NotificationsPage: notifications & stats
    NotificationsPage->>User: render notifications list
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through CORS and scrubbed the stream,
DOMPurify singing, no script can scheme,
Spinners twirl while docs sit tight,
Packages pinned, the build takes flight,
A cheerful rabbit guards the night.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title '[chore]: security dependency update and maintenance' directly matches the main objective of the PR, which is to perform security-floor dependency updates and maintenance across multiple packages.
Description check ✅ Passed The description includes all key template sections: a summary of changes (security-floor dependency update), type of change checkboxes (Bug fix, Refactor, Other selected), testing verification (all boxes checked), and a detailed 'Notes for reviewers' clarifying the scope and intent.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/update-packages

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (5)
src/app/notifications/NotificationsPage.tsx (1)

40-46: 💤 Low value

Simplify parseSelectedCategory by checking enum membership directly.

The four-way if-chain duplicates every enum member by name and will silently rot if a new NotificationCategory variant is added — the new value would be coerced to 'all' instead of itself. A single membership check against the enum's values keeps it data-driven:

♻️ Proposed simplification
-function parseSelectedCategory(value: string): NotificationCategory | 'all' {
-  if (value === NotificationCategory.ENGAGEMENT) return NotificationCategory.ENGAGEMENT
-  if (value === NotificationCategory.CONTENT) return NotificationCategory.CONTENT
-  if (value === NotificationCategory.SYSTEM) return NotificationCategory.SYSTEM
-  if (value === NotificationCategory.MODERATION) return NotificationCategory.MODERATION
-  return 'all'
-}
+const CATEGORY_VALUES = Object.values(NotificationCategory) as NotificationCategory[]
+
+function parseSelectedCategory(value: string): NotificationCategory | 'all' {
+  return (CATEGORY_VALUES as string[]).includes(value) ? (value as NotificationCategory) : 'all'
+}
🤖 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 `@src/app/notifications/NotificationsPage.tsx` around lines 40 - 46, The
function parseSelectedCategory repeats each NotificationCategory member and will
miss future enum values; change it to perform a membership check against the
enum values (e.g. use Object.values(NotificationCategory).includes(value)) and,
if present, return the value cast to NotificationCategory, otherwise return
'all'—update parseSelectedCategory to rely on enum membership rather than
hard-coded branches.
src/app/docs/api/swagger/SwaggerPage.tsx (1)

8-8: 💤 Low value

Bundle the 563 KB OpenAPI spec only when needed, not in the initial page chunk.

Two concerns with this static import:

  1. Non-idiomatic path traversal: Importing from public/ via ../../../../../ is brittle (any file move breaks it) and violates the intended use of public/ for static asset serving, not module resolution.

  2. Deferred loading lost: Switching from url to configuration.content = openApiDocument bundles the ~563 KB spec into the page's client JavaScript chunk, downloaded by every visitor. The component itself is lazy-loaded (line 13), but the spec payload is not, negating the performance benefit of deferred loading.

Suggested fix: Move mobile-openapi.json to src/ with a path alias (e.g., @/server/api), or restore url-based loading so Scalar fetches the spec on demand—especially given the file size.

🤖 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 `@src/app/docs/api/swagger/SwaggerPage.tsx` at line 8, The static import of
mobile-openapi.json in SwaggerPage.tsx (openApiDocument) pulls a 563KB spec into
the initial client bundle and uses a brittle relative path into public/;
instead, stop the direct import and either (A) move the JSON into src/ and
import it via your project path alias (e.g., from `@/server/api`) so it becomes a
proper module you can lazy-load, or (B) restore URL-based loading so the
component sets configuration.content by fetching the JSON at runtime (keep the
lazy-loaded component intact) and assign configuration.content only after the
fetch resolves; also remove the ../../../../../public import and update any
references to openApiDocument and configuration.content accordingly.
src/utils/markdown.ts (1)

8-10: ⚖️ Poor tradeoff

The workaround is partially redundant; jsdom is already marked as server-external in next.config.js.

serverExternalPackages: ['jsdom'] is already configured, which mitigates the bundling risk. However, the fragile 'js' + 'dom' concatenation and __non_webpack_require__ workaround remain unnecessary. Simplify to a plain import { JSDOM } from 'jsdom' at the top of the file, since jsdom is already guaranteed to be server-external.

🤖 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 `@src/utils/markdown.ts` around lines 8 - 10, Remove the fragile runtime
workaround using __non_webpack_require__ and the JSDOM_PACKAGE_NAME
concatenation and replace it with a direct top-level import; specifically delete
the declare __non_webpack_require__ and const JSDOM_PACKAGE_NAME = 'js' + 'dom'
lines and add a plain import { JSDOM } from 'jsdom' at the top so the file uses
the JSDOM symbol directly (serverExternalPackages already prevents bundling).
src/utils/markdown.test.ts (2)

153-158: 💤 Low value

Consider tightening the data: assertion to avoid false negatives.

expect(result).not.toContain('data:') will pass if the sanitizer simply HTML-encodes the colon (e.g., data&#58;) or any other encoding form, even though the underlying URI would still resolve in some contexts. Since the input also contains a base64-encoded <script> payload (PHNjcmlwdD4=), it might be worth additionally asserting not.toContain('PHNjcmlwdD4=') (or the decoded <script>) to ensure the full payload is dropped rather than just the scheme literal.

♻️ Suggested additional assertion
     it('should strip unsafe data URI markdown links', () => {
       const result = parseMarkdown('[bad](data:text/html;base64,PHNjcmlwdD4=)')

       expect(result).not.toContain('data:')
       expect(result).not.toContain('href=')
+      expect(result).not.toContain('PHNjcmlwdD4=')
     })
🤖 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 `@src/utils/markdown.test.ts` around lines 153 - 158, The test "should strip
unsafe data URI markdown links" currently only asserts absence of the literal
"data:" which can be evaded by encoding; update the test around
parseMarkdown('[bad](data:text/html;base64,PHNjcmlwdD4=)') to also assert that
the base64 payload and decoded tag are removed — e.g., add negative assertions
for the base64 chunk "PHNjcmlwdD4=" and for the decoded "<script>" token — so
parseMarkdown is verified to drop the actual payload, not just the scheme
literal.

169-178: ⚡ Quick win

Wall-clock ReDoS assertion is likely to flake on shared CI runners.

expect(duration).toBeLessThan(1000) is sensitive to runner load, GC pauses, and noisy neighbors. A 30k-char linkify input that normally completes in tens of milliseconds locally can occasionally cross 1s on a contended CI VM, leading to false positives that look like security regressions. A few options to make this more robust:

  • Raise the budget to something clearly catastrophic (e.g., 3–5s) so only true ReDoS regressions trip it, while still failing fast on quadratic/exponential blowups.
  • Combine the time bound with a process-level timeout (Vitest { timeout }) and prefer performance.now() over Date.now() for sub-ms precision.
  • Optionally gate strict timing in CI via an env flag, or mark the test as it.concurrent only when timing is loose.
♻️ Suggested adjustment
-    it('should handle markdown-it linkify ReDoS input quickly', () => {
-      const redosInput = `https://example.com/${'*'.repeat(30000)}!`
-      const start = Date.now()
-
-      const result = parseMarkdown(redosInput)
-      const duration = Date.now() - start
-
-      expect(typeof result).toBe('string')
-      expect(duration).toBeLessThan(1000)
-    })
+    it(
+      'should handle markdown-it linkify ReDoS input quickly',
+      () => {
+        const redosInput = `https://example.com/${'*'.repeat(30000)}!`
+        const start = performance.now()
+
+        const result = parseMarkdown(redosInput)
+        const duration = performance.now() - start
+
+        expect(typeof result).toBe('string')
+        // Generous bound: catches catastrophic backtracking without
+        // flaking on contended CI runners.
+        expect(duration).toBeLessThan(3000)
+      },
+      5000,
+    )
🤖 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 `@src/utils/markdown.test.ts` around lines 169 - 178, The wall-clock assertion
in the test for parseMarkdown is flaky; switch timing to use performance.now()
instead of Date.now(), raise the allowed duration to a safer threshold (e.g.,
3000 ms) to avoid CI noise, and add a Vitest per-test timeout (e.g., set the
it(...) timeout to 5000 ms) so the test fails by the test runner on catastrophic
hangs rather than flaky assertions; update the test case surrounding
parseMarkdown(redosInput) to compute duration via performance.now(), assert
duration < 3000, and pass a timeout option to the it call.
🤖 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 `@src/lib/cors.ts`:
- Around line 80-91: The isAllowedRequestOrigin function signature includes
requestOrigin but never uses it, so either implement same-origin logic or remove
the dead parameter: Option A (honor same-origin) — inside isAllowedRequestOrigin
(and using getOriginFromUrl) add an early check that if params.requestOrigin
equals the parsed sourceOrigin (or if requestOrigin is present and is included
in allowedOrigins) return true, then fall back to existing source parsing and
allowedOrigins checks; update tests in src/lib/cors.test.ts to assert
same-origin behavior. Option B (remove param) — remove requestOrigin from the
params shape of isAllowedRequestOrigin, drop passing req.nextUrl.origin from the
middleware call sites in middleware.ts, and update the tests to stop relying on
a requestOrigin argument. Choose one approach and make corresponding changes to
isAllowedRequestOrigin, its call sites, and tests (referenced symbols:
isAllowedRequestOrigin, getOriginFromUrl, and the middleware call that passes
req.nextUrl.origin).

In `@src/utils/markdown.ts`:
- Around line 108-123: The code calls process.getBuiltinModule directly in
getServerRequire (used by loadJSDOM), which doesn't exist on Node 20; change
getServerRequire to feature-detect process.getBuiltinModule (typeof
process.getBuiltinModule === 'function') and only call it when available,
otherwise fall back to requiring the built-in Module via plain require('module')
and use its createRequire; keep the existing error path if neither approach
yields a Module and preserve how loadJSDOM still prefers __non_webpack_require__
and then getServerRequire(JSDOM_PACKAGE_NAME).JSDOM so behavior and identifiers
(loadJSDOM, getServerRequire, JSDOM_PACKAGE_NAME, __non_webpack_require__)
remain the same.
- Around line 75-88: The pre-check using DANGEROUS_PATTERNS /
DANGEROUS_TEXT_REPLACEMENTS and the short‑circuit via containsDangerousInput
that forces parseMarkdown and stripMarkdown to call sanitizePlainText should be
removed: stop gating parseMarkdown and stripMarkdown on containsDangerousInput
and let DOMPurify’s ALLOWED_ATTR whitelist handle sanitization; remove or stop
using the DANGEROUS_* arrays and the branch that collapses to sanitizePlainText
(but if you need a fast deny-path, move any heuristic checks to the rendered
HTML sanitization step, e.g., inspect the produced DOM/html for <script> or
suspicious attribute values instead of the raw markdown).

---

Nitpick comments:
In `@src/app/docs/api/swagger/SwaggerPage.tsx`:
- Line 8: The static import of mobile-openapi.json in SwaggerPage.tsx
(openApiDocument) pulls a 563KB spec into the initial client bundle and uses a
brittle relative path into public/; instead, stop the direct import and either
(A) move the JSON into src/ and import it via your project path alias (e.g.,
from `@/server/api`) so it becomes a proper module you can lazy-load, or (B)
restore URL-based loading so the component sets configuration.content by
fetching the JSON at runtime (keep the lazy-loaded component intact) and assign
configuration.content only after the fetch resolves; also remove the
../../../../../public import and update any references to openApiDocument and
configuration.content accordingly.

In `@src/app/notifications/NotificationsPage.tsx`:
- Around line 40-46: The function parseSelectedCategory repeats each
NotificationCategory member and will miss future enum values; change it to
perform a membership check against the enum values (e.g. use
Object.values(NotificationCategory).includes(value)) and, if present, return the
value cast to NotificationCategory, otherwise return 'all'—update
parseSelectedCategory to rely on enum membership rather than hard-coded
branches.

In `@src/utils/markdown.test.ts`:
- Around line 153-158: The test "should strip unsafe data URI markdown links"
currently only asserts absence of the literal "data:" which can be evaded by
encoding; update the test around
parseMarkdown('[bad](data:text/html;base64,PHNjcmlwdD4=)') to also assert that
the base64 payload and decoded tag are removed — e.g., add negative assertions
for the base64 chunk "PHNjcmlwdD4=" and for the decoded "<script>" token — so
parseMarkdown is verified to drop the actual payload, not just the scheme
literal.
- Around line 169-178: The wall-clock assertion in the test for parseMarkdown is
flaky; switch timing to use performance.now() instead of Date.now(), raise the
allowed duration to a safer threshold (e.g., 3000 ms) to avoid CI noise, and add
a Vitest per-test timeout (e.g., set the it(...) timeout to 5000 ms) so the test
fails by the test runner on catastrophic hangs rather than flaky assertions;
update the test case surrounding parseMarkdown(redosInput) to compute duration
via performance.now(), assert duration < 3000, and pass a timeout option to the
it call.

In `@src/utils/markdown.ts`:
- Around line 8-10: Remove the fragile runtime workaround using
__non_webpack_require__ and the JSDOM_PACKAGE_NAME concatenation and replace it
with a direct top-level import; specifically delete the declare
__non_webpack_require__ and const JSDOM_PACKAGE_NAME = 'js' + 'dom' lines and
add a plain import { JSDOM } from 'jsdom' at the top so the file uses the JSDOM
symbol directly (serverExternalPackages already prevents bundling).
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 81d84f57-cd91-4f8b-95c0-0906bd428990

📥 Commits

Reviewing files that changed from the base of the PR and between cadbe76 and b5d01aa.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (10)
  • package.json
  • src/app/docs/api/swagger/SwaggerPage.tsx
  • src/app/notifications/NotificationsPage.tsx
  • src/app/v2/listings/components/ListingsHeader.tsx
  • src/lib/cors.test.ts
  • src/lib/cors.ts
  • src/middleware.ts
  • src/utils/markdown.node.test.ts
  • src/utils/markdown.test.ts
  • src/utils/markdown.ts

Comment thread src/lib/cors.ts
Comment thread src/utils/markdown.ts Outdated
Comment thread src/utils/markdown.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/utils/markdown.ts (1)

178-209: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace the raw-input dangerous-pattern gate in validateMarkdown and fix the user-facing error.

This block reintroduces the same false-positive problem that was just removed from parseMarkdown/stripMarkdown, only here it surfaces as errors/isValid=false:

  1. False positives on benign prose. Patterns like /javascript:/i, /vbscript:/i, /<script/i, /onload=/i, /onerror=/i, /data:.*base64/i match any markdown that describes these schemes (e.g., a doc note about "Never use javascript: URLs", a code block showing <script>, or text containing onload=true). These will be reported as invalid even though parseMarkdown (line 194) already routes through DOMPurify with the allowlisted PURIFY_CONFIG, which is the actual security boundary.
  2. Unprofessional/joke error string. 'Masterhacker detected.' (line 189) is user-visible via the returned errors array and is neither actionable nor appropriate for production UI/logs.
  3. Duplicated error pushes. The forEach loop pushes the same string once per matching pattern, so a single input that hits both <script and onload= yields errors.length === 2 with identical entries.

DOMPurify’s whitelist already strips event handlers, dangerous URI schemes, and <script>/<object>/<embed> etc., so the regex layer adds no security but breaks legitimate inputs and validation UX. If a fast deny-path is genuinely required, run the heuristic against the rendered HTML (or compare pre/post-sanitization output) rather than the raw markdown source, and use a single, generic, deduplicated message.

♻️ Suggested simplification
 export function validateMarkdown(markdownText: string): {
   isValid: boolean
   cleanText: string
   errors: string[]
 } {
   const errors: string[] = []

   if (!markdownText) {
     return { isValid: true, cleanText: '', errors: [] }
   }

-  const dangerousPatterns = [
-    /javascript:/i,
-    /data:.*base64/i,
-    /vbscript:/i,
-    /<script/i,
-    /onload=/i,
-    /onerror=/i,
-  ]
-
-  dangerousPatterns.forEach((pattern) => {
-    if (pattern.test(markdownText)) {
-      errors.push('Masterhacker detected.')
-    }
-  })
-
   try {
     const cleanText = parseMarkdown(markdownText)
     return {
       isValid: errors.length === 0,
       cleanText,
       errors,
     }
   } catch (error) {
     console.warn('Markdown validation failed:', error)
     errors.push('Invalid markdown syntax')
     return {
       isValid: false,
       cleanText: markdownText,
       errors,
     }
   }
 }

If a "potentially unsafe content was removed" signal is still required, derive it by comparing md.render(markdownText) to the DOMPurify output and emitting a single generic message when they differ.

🤖 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 `@src/utils/markdown.ts` around lines 178 - 209, The validateMarkdown function
currently scans raw markdownText with dangerousPatterns and pushes the jokey
'Masterhacker detected.' message repeatedly; remove that raw-input regex gate
and instead detect removed/modified dangerous content by comparing the rendered
HTML from md.render(markdownText) (or parseMarkdown output) with the
DOMPurify-sanitized cleanText returned by parseMarkdown/stripMarkdown, and if
they differ push a single, professional, generic error into errors (e.g.,
"Potentially unsafe content was removed") and ensure only one entry is added
(dedupe), leaving parseMarkdown/stripMarkdown/DOMPurify as the primary security
boundary.
🤖 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.

Outside diff comments:
In `@src/utils/markdown.ts`:
- Around line 178-209: The validateMarkdown function currently scans raw
markdownText with dangerousPatterns and pushes the jokey 'Masterhacker
detected.' message repeatedly; remove that raw-input regex gate and instead
detect removed/modified dangerous content by comparing the rendered HTML from
md.render(markdownText) (or parseMarkdown output) with the DOMPurify-sanitized
cleanText returned by parseMarkdown/stripMarkdown, and if they differ push a
single, professional, generic error into errors (e.g., "Potentially unsafe
content was removed") and ensure only one entry is added (dedupe), leaving
parseMarkdown/stripMarkdown/DOMPurify as the primary security boundary.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e5852d7b-0790-4085-ba3f-5199c36e6022

📥 Commits

Reviewing files that changed from the base of the PR and between b5d01aa and 4527d8a.

📒 Files selected for processing (7)
  • Dockerfile
  • src/lib/cors.test.ts
  • src/lib/cors.ts
  • src/middleware.ts
  • src/utils/markdown.node.test.ts
  • src/utils/markdown.test.ts
  • src/utils/markdown.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/utils/markdown.node.test.ts
  • src/lib/cors.test.ts
  • src/lib/cors.ts
  • src/middleware.ts
  • src/utils/markdown.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
src/utils/markdown.ts (3)

10-10: 💤 Low value

Add a brief comment explaining the 'js' + 'dom' concatenation.

The string concatenation is a deliberate trick to prevent webpack from statically analyzing and bundling jsdom into client output, but this intent is non-obvious. A short comment will save future maintainers from "simplifying" it back to a literal.

📝 Proposed comment
-const JSDOM_PACKAGE_NAME = 'js' + 'dom'
+// Concatenated to hide the literal from webpack's static analyzer so `jsdom`
+// is never bundled into client output; it must only be resolved at runtime on the server.
+const JSDOM_PACKAGE_NAME = 'js' + 'dom'
🤖 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 `@src/utils/markdown.ts` at line 10, Add a brief inline comment above the
JSDOM_PACKAGE_NAME constant explaining that the 'js' + 'dom' concatenation is
intentional to avoid static bundlers (e.g., webpack) from detecting and bundling
the "jsdom" package into client builds; reference the constant name
JSDOM_PACKAGE_NAME and keep the comment short and explicit about the bundling
avoidance intent.

101-112: 💤 Low value

Redundant guard, but functionally correct.

The typeof process.getBuiltinModule !== 'function' check at line 102 and the if (!moduleBuiltin) check at line 107 throw the same error. In practice, when getBuiltinModule exists, calling it with 'module' will always return the builtin (or throw ERR_UNKNOWN_BUILTIN_MODULE), so the second guard is unreachable defensive coding. Not worth changing on its own, but consider consolidating if you touch this again.

🤖 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 `@src/utils/markdown.ts` around lines 101 - 112, The getServerRequire function
contains a redundant second guard that checks moduleBuiltin after verifying
process.getBuiltinModule exists; consolidate the checks by either using a single
try/catch around calling process.getBuiltinModule('module') or keep the initial
typeof check and remove the unreachable if (!moduleBuiltin) branch. Update
getServerRequire to call process.getBuiltinModule('module') inside a try block
(or after the typeof check) and throw the same Error('Node module loader is
unavailable for server-side markdown sanitization') if obtaining the builtin
fails, then return moduleBuiltin.createRequire(`${process.cwd()}/package.json`)
as before.

122-165: 💤 Low value

Fallback in catch reuses the same DOMPurify instance that may have caused the failure.

If getPurify() ever throws during initialization (e.g., jsdom resolution fails at runtime on the server path), sanitizeHtml(html) throws on line 128, then the catch invokes sanitizePlainText(markdownText) which also calls getPurify() — re-throwing the exact same error and propagating out of parseMarkdown. Same applies to stripMarkdown at line 163.

This is a narrow edge case (only matters when DOMPurify init itself fails, not when markdown-it fails), so it's a defense-in-depth concern rather than a blocker. If you want a truly safe terminal fallback, escape with a tiny inline helper instead of routing back through DOMPurify:

♻️ Optional safer fallback
+function escapeHtml(text: string): string {
+  return text
+    .replace(/&/g, '&amp;')
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;')
+    .replace(/"/g, '&quot;')
+    .replace(/'/g, '&#x27;')
+}
+
 export function parseMarkdown(markdownText: string): string {
   if (!markdownText) return ''
 
   try {
     const html = md.render(markdownText)
     return sanitizeHtml(html)
   } catch (error) {
     console.warn('Markdown parsing failed:', error)
-    return sanitizePlainText(markdownText)
+    return escapeHtml(markdownText)
   }
 }
🤖 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 `@src/utils/markdown.ts` around lines 122 - 165, parseMarkdown and
stripMarkdown currently call sanitizePlainText in their catch blocks which in
turn invokes getPurify(), so if DOMPurify/init fails the fallback will re-throw
the same error; replace those fallbacks with a safe, pure-JS escape that does
not call getPurify() (e.g., a small inline escapeHtml helper that replaces &, <,
>, ", ' and collapses whitespace/newlines to single spaces) and return that
escaped string from the catch in both parseMarkdown and stripMarkdown; keep
references to md.render usage intact but ensure the catch never calls
sanitizePlainText or any function that calls getPurify().
🤖 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.

Nitpick comments:
In `@src/utils/markdown.ts`:
- Line 10: Add a brief inline comment above the JSDOM_PACKAGE_NAME constant
explaining that the 'js' + 'dom' concatenation is intentional to avoid static
bundlers (e.g., webpack) from detecting and bundling the "jsdom" package into
client builds; reference the constant name JSDOM_PACKAGE_NAME and keep the
comment short and explicit about the bundling avoidance intent.
- Around line 101-112: The getServerRequire function contains a redundant second
guard that checks moduleBuiltin after verifying process.getBuiltinModule exists;
consolidate the checks by either using a single try/catch around calling
process.getBuiltinModule('module') or keep the initial typeof check and remove
the unreachable if (!moduleBuiltin) branch. Update getServerRequire to call
process.getBuiltinModule('module') inside a try block (or after the typeof
check) and throw the same Error('Node module loader is unavailable for
server-side markdown sanitization') if obtaining the builtin fails, then return
moduleBuiltin.createRequire(`${process.cwd()}/package.json`) as before.
- Around line 122-165: parseMarkdown and stripMarkdown currently call
sanitizePlainText in their catch blocks which in turn invokes getPurify(), so if
DOMPurify/init fails the fallback will re-throw the same error; replace those
fallbacks with a safe, pure-JS escape that does not call getPurify() (e.g., a
small inline escapeHtml helper that replaces &, <, >, ", ' and collapses
whitespace/newlines to single spaces) and return that escaped string from the
catch in both parseMarkdown and stripMarkdown; keep references to md.render
usage intact but ensure the catch never calls sanitizePlainText or any function
that calls getPurify().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7401a8e4-ca5d-4cb0-8159-76aeb4fa848c

📥 Commits

Reviewing files that changed from the base of the PR and between 4527d8a and 943e35b.

📒 Files selected for processing (4)
  • .vercelignore
  • src/middleware.ts
  • src/utils/markdown.test.ts
  • src/utils/markdown.ts
✅ Files skipped from review due to trivial changes (1)
  • .vercelignore
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/middleware.ts
  • src/utils/markdown.test.ts

@Producdevity Producdevity merged commit aeecf27 into staging May 11, 2026
5 checks passed
@github-project-automation github-project-automation Bot moved this to Done in EmuReady May 11, 2026
@Producdevity Producdevity deleted the chore/update-packages branch May 11, 2026 19:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants