Skip to content

Improve footer sync and show Creem webhook URL - #3236

Closed
asdv23 wants to merge 5 commits into
QuantumNous:mainfrom
asdv23:codex/creem-webhook-readonly
Closed

Improve footer sync and show Creem webhook URL#3236
asdv23 wants to merge 5 commits into
QuantumNous:mainfrom
asdv23:codex/creem-webhook-readonly

Conversation

@asdv23

@asdv23 asdv23 commented Mar 12, 2026

Copy link
Copy Markdown

Summary

  • support footer HTML links as iframe embeds and keep footer-related status/localStorage updates in sync
  • show a read-only Creem webhook callback URL in payment settings, matching the Stripe UX pattern
  • add small helper tests for footer mode detection, status option patching, and payment webhook URL generation

Testing

  • node --test web/src/helpers/footer.test.mjs web/src/helpers/statusOption.test.mjs web/src/helpers/paymentWebhook.test.mjs

Summary by CodeRabbit

  • New Features

    • Footer can render external pages (iframe), raw HTML, or a fallback inline footer.
    • Creem payment settings now display the webhook URL and include product management UI.
  • Improvements

    • Footer input hints clarify HTML or page links are supported.
    • Status option syncing and footer normalization/selection behavior improved.
  • Tests

    • Unit tests added for footer mode detection, status-option mapping, and webhook URL builder.

@coderabbitai

coderabbitai Bot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

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

Refactors footer rendering to compute a normalized footer value and render mode (iframe / html / default), synchronizes iframe height after load, adds footer helpers and tests, introduces status-option patching and sync in settings, adds payment webhook URL helper/tests and surfaces the webhook URL in Creem settings, plus i18n keys.

Changes

Cohort / File(s) Summary
Footer Rendering System
web/src/components/layout/Footer.jsx, web/src/helpers/footer.js, web/src/helpers/footer.test.mjs, web/src/index.css
Add normalizeFooterValue and getFooterRenderMode; compute footerValue/footerRenderMode; render footer as iframe (with dynamic height sync), direct HTML, or default inline; add .custom-footer-frame CSS and unit tests.
Settings & OtherSetting UI
web/src/components/settings/OtherSetting.jsx
Add status synchronization after option updates, new update-check / GitHub release UI and helpers, expanded submit handlers (notice, agreement, policies, logo, footer, etc.), and inline documentation; update footer placeholder hint.
Status Option Helpers & Tests
web/src/helpers/statusOption.js, web/src/helpers/statusOption.test.mjs
Add STATUS_OPTION_FIELD_MAP and getStatusOptionPatch to map option keys to status/storage keys and produce normalized patch objects; include unit tests.
Payment Webhook Helper & UI
web/src/helpers/paymentWebhook.js, web/src/helpers/paymentWebhook.test.mjs, web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.jsx
Add getPaymentWebhookUrl(serverAddress, provider, fallbackBaseLabel) (normalizes addresses and builds webhook path); add tests and display computed webhook URL in Creem payment settings UI.
i18n Locales
web/src/i18n/locales/*.json
web/src/i18n/locales/en.json, .../fr.json, .../ja.json, .../ru.json, .../vi.json, .../zh-CN.json, .../zh-TW.json
Add translation key "Webhook 填:" across multiple locale files for the webhook label.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant FooterComp as Footer Component
  participant Helper as Footer Helper
  participant External as External Page (iframe)

  User->>FooterComp: mount/render (status.footer_html)
  FooterComp->>Helper: normalizeFooterValue(status.footer_html) / getFooterRenderMode(...)
  Helper-->>FooterComp: footerValue, renderMode
  alt renderMode == iframe
    FooterComp->>External: set iframe src = footerValue
    External-->>FooterComp: iframe load event
    FooterComp->>FooterComp: measure iframe content height
    FooterComp-->>User: update iframe height / render footer
  else renderMode == html
    FooterComp-->>User: inject footerValue as HTML
  else default
    FooterComp-->>User: render default inline footer
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • seefs001
  • creamlike1024

Poem

🐰 I hop and patch the footer bright,
I frame the page and tune its height,
I weave the webhook, neat and clever,
Status whispers held together,
A carrot cheer for code forever! 🥕✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: improving footer synchronization and displaying the Creem webhook URL.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

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
Contributor

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 (2)
web/src/components/layout/Footer.jsx (2)

45-61: Consider adding a comment about cross-origin iframe limitation.

The empty catch block silently handles the expected SecurityError when accessing contentDocument on cross-origin iframes. This is correct behavior, but future maintainers might wonder why the catch is empty.

For cross-origin footer URLs, the iframe will retain the default 240px height since contentDocument access will be blocked.

📝 Suggested documentation improvement
   const handleFooterFrameLoad = useCallback(() => {
     try {
       const iframeDocument = iframeRef.current?.contentDocument;
       if (!iframeDocument) {
         return;
       }

       const nextHeight = Math.max(
         iframeDocument.documentElement?.scrollHeight || 0,
         iframeDocument.body?.scrollHeight || 0,
       );

       if (nextHeight > 0) {
         setIframeHeight(nextHeight);
       }
-    } catch {}
+    } catch {
+      // Cross-origin iframes throw SecurityError; fallback to default height
+    }
   }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/layout/Footer.jsx` around lines 45 - 61, The empty catch
in handleFooterFrameLoad silently swallows the expected SecurityError when
accessing iframeRef.current?.contentDocument for cross-origin footer URLs;
update the catch block to include a concise comment referencing this
cross-origin limitation and that in such cases the iframe will keep the default
240px height (set via setIframeHeight elsewhere), e.g., mention SecurityError /
cross-origin access is expected and intentionally ignored so future maintainers
understand why the catch is empty; keep the behavior unchanged (do not rethrow).

244-251: Consider adding sandbox attribute for defense-in-depth.

The iframe loads admin-configured URLs without a sandbox attribute. While this content is admin-controlled, adding sandbox with appropriate permissions provides defense-in-depth against compromised footer URLs.

🛡️ Optional security hardening
             <iframe
               ref={iframeRef}
               title={t('页脚')}
               src={footerValue}
               className='custom-footer-frame'
               style={{ height: `${iframeHeight}px` }}
               onLoad={handleFooterFrameLoad}
+              sandbox="allow-scripts allow-same-origin"
             />

Note: allow-same-origin is needed for the height auto-adjustment to work on same-origin iframes. Adjust permissions based on what footer content actually needs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/layout/Footer.jsx` around lines 244 - 251, The iframe
rendering in Footer.jsx (the element using iframeRef, title via t('页脚'),
src={footerValue}, className='custom-footer-frame' and
onLoad={handleFooterFrameLoad}) should include a sandbox attribute for
defense-in-depth; update the JSX to add sandbox with a minimal permission set
(e.g. include allow-scripts if scripts are required) and, only if you need the
current auto-height behaviour for same-origin content, include allow-same-origin
as well—adjust the permissions to the least needed for the footer content.
Ensure the sandbox prop is added to the same iframe element so height adjustment
(handled by handleFooterFrameLoad) still works for same-origin when
allow-same-origin is permitted.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/helpers/paymentWebhook.js`:
- Around line 1-7: The helper getPaymentWebhookUrl currently embeds a UI
placeholder '网站地址'; remove this presentation text and make the function
presentation-agnostic by returning either the normalizedServerAddress or, if
empty, a caller-supplied fallback string; update the signature to accept an
optional fallback param (e.g., getPaymentWebhookUrl(serverAddress, provider,
fallback) ) and use fallback when normalizedServerAddress is falsy so UI
components can pass translated text via useTranslation()/t('...') instead of
hardcoding Chinese here.

In `@web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.jsx`:
- Around line 272-275: The Banner description currently uses a hard-coded
Chinese label "Webhook 填:" which bypasses i18n; import and call useTranslation()
in the SettingsPaymentGatewayCreem component to get t, replace the inline string
with t('Webhook 填:') and compose the description as `${t('Webhook
填:')}${getPaymentWebhookUrl(props.options.ServerAddress, 'creem')}`, and add the
key "Webhook 填:" to the locale JSON files under web/src/i18n/locales/{lang}.json
(use the Chinese string as the key) so the banner is translatable; ensure Banner
and getPaymentWebhookUrl usage remains unchanged.
- Around line 272-275: The Banner currently dereferences
props.options.ServerAddress when rendering the Creem webhook URL which can throw
if props.options is undefined; update the rendering to guard props.options (for
example using props.options && props.options.ServerAddress or optional chaining
props.options?.ServerAddress) before calling getPaymentWebhookUrl, or
conditionally render the Banner only when ServerAddress is present; reference
the Banner JSX and the getPaymentWebhookUrl call in
SettingsPaymentGatewayCreem.jsx and ensure the description string uses a safe
fallback (e.g., empty string or "loading...") until props.options.ServerAddress
is available.

---

Nitpick comments:
In `@web/src/components/layout/Footer.jsx`:
- Around line 45-61: The empty catch in handleFooterFrameLoad silently swallows
the expected SecurityError when accessing iframeRef.current?.contentDocument for
cross-origin footer URLs; update the catch block to include a concise comment
referencing this cross-origin limitation and that in such cases the iframe will
keep the default 240px height (set via setIframeHeight elsewhere), e.g., mention
SecurityError / cross-origin access is expected and intentionally ignored so
future maintainers understand why the catch is empty; keep the behavior
unchanged (do not rethrow).
- Around line 244-251: The iframe rendering in Footer.jsx (the element using
iframeRef, title via t('页脚'), src={footerValue}, className='custom-footer-frame'
and onLoad={handleFooterFrameLoad}) should include a sandbox attribute for
defense-in-depth; update the JSX to add sandbox with a minimal permission set
(e.g. include allow-scripts if scripts are required) and, only if you need the
current auto-height behaviour for same-origin content, include allow-same-origin
as well—adjust the permissions to the least needed for the footer content.
Ensure the sandbox prop is added to the same iframe element so height adjustment
(handled by handleFooterFrameLoad) still works for same-origin when
allow-same-origin is permitted.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 442753cf-9137-43a1-8c67-6fe8fc8da528

📥 Commits

Reviewing files that changed from the base of the PR and between 4e1b05e and 18d06c0.

📒 Files selected for processing (10)
  • web/src/components/layout/Footer.jsx
  • web/src/components/settings/OtherSetting.jsx
  • web/src/helpers/footer.js
  • web/src/helpers/footer.test.mjs
  • web/src/helpers/paymentWebhook.js
  • web/src/helpers/paymentWebhook.test.mjs
  • web/src/helpers/statusOption.js
  • web/src/helpers/statusOption.test.mjs
  • web/src/index.css
  • web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.jsx

Comment thread web/src/helpers/paymentWebhook.js Outdated
Comment thread web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.jsx
@asdv23

asdv23 commented Mar 13, 2026

Copy link
Copy Markdown
Author

Also addressed the low-risk Footer cleanup in d7d1900 by documenting why the empty catch is intentional for cross-origin iframe sizing failures. I left the iframe sandbox suggestion out of this PR because it can change behavior for admin-configured footer embeds; that one is better handled as a separate hardening pass if maintainers want it.

@asdv23

asdv23 commented Mar 13, 2026

Copy link
Copy Markdown
Author

@coderabbitai run pre-merge checks

@coderabbitai

coderabbitai Bot commented Mar 13, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Running Pre-merge checks. Results will be posted in the summary comment once complete.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
web/src/helpers/footer.js (1)

41-43: Harden iframe URL detection to valid URLs only.

Current prefix matching can classify malformed strings as iframe. Consider parsing via URL and checking protocol explicitly.

🔧 Suggested refactor
-  if (/^https?:\/\//i.test(normalizedFooter)) {
-    return 'iframe';
-  }
+  try {
+    const parsed = new URL(normalizedFooter);
+    if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
+      return 'iframe';
+    }
+  } catch {
+    // Not a valid URL; fall through to HTML mode.
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/helpers/footer.js` around lines 41 - 43, Replace the brittle regex
prefix check that returns 'iframe' for normalizedFooter with a URL-parse guard:
attempt to construct new URL(normalizedFooter) in a try/catch and only return
'iframe' if the parsed URL.protocol is exactly 'http:' or 'https:'; on any
thrown error or non-matching protocol, do not classify as 'iframe'. Update the
logic around the existing normalizedFooter check (the block containing
/^https?:\/\//i.test(normalizedFooter)) to use this safe parse-and-check
approach.
web/src/helpers/paymentWebhook.js (1)

35-36: Normalize fallback input to avoid accidental double slashes.

If a caller passes a fallback ending with /, the generated URL can include //api/.... Consider normalizing the fallback the same way as serverAddress.

♻️ Optional patch
   const normalizedServerAddress = String(serverAddress || '')
     .trim()
     .replace(/\/+$/, '');
-  const baseUrl = normalizedServerAddress || fallbackBaseLabel;
+  const normalizedFallbackBaseLabel = String(fallbackBaseLabel || '')
+    .trim()
+    .replace(/\/+$/, '');
+  const baseUrl = normalizedServerAddress || normalizedFallbackBaseLabel;
   return `${baseUrl}/api/${provider}/webhook`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/helpers/paymentWebhook.js` around lines 35 - 36, The
fallbackBaseLabel can include a trailing slash which leads to double slashes in
the returned URL; update the logic that computes baseUrl so fallbackBaseLabel is
normalized the same way as normalizedServerAddress (trim any trailing slash)
before composing the webhook URL. Locate the code that defines
normalizedServerAddress and reuse its normalization (or apply the same trim) to
produce a cleaned fallbackBaseLabel, then compute baseUrl =
normalizedServerAddress || cleanedFallbackBaseLabel and return
`${baseUrl}/api/${provider}/webhook`.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@web/src/helpers/footer.js`:
- Around line 41-43: Replace the brittle regex prefix check that returns
'iframe' for normalizedFooter with a URL-parse guard: attempt to construct new
URL(normalizedFooter) in a try/catch and only return 'iframe' if the parsed
URL.protocol is exactly 'http:' or 'https:'; on any thrown error or non-matching
protocol, do not classify as 'iframe'. Update the logic around the existing
normalizedFooter check (the block containing
/^https?:\/\//i.test(normalizedFooter)) to use this safe parse-and-check
approach.

In `@web/src/helpers/paymentWebhook.js`:
- Around line 35-36: The fallbackBaseLabel can include a trailing slash which
leads to double slashes in the returned URL; update the logic that computes
baseUrl so fallbackBaseLabel is normalized the same way as
normalizedServerAddress (trim any trailing slash) before composing the webhook
URL. Locate the code that defines normalizedServerAddress and reuse its
normalization (or apply the same trim) to produce a cleaned fallbackBaseLabel,
then compute baseUrl = normalizedServerAddress || cleanedFallbackBaseLabel and
return `${baseUrl}/api/${provider}/webhook`.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b3c229a3-867d-4ef8-a03e-2ee60961c9f2

📥 Commits

Reviewing files that changed from the base of the PR and between d7d1900 and 96af2a0.

📒 Files selected for processing (4)
  • web/src/helpers/footer.js
  • web/src/helpers/paymentWebhook.js
  • web/src/helpers/statusOption.js
  • web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.jsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • web/src/helpers/statusOption.js
  • web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.jsx

@asdv23

asdv23 commented Mar 13, 2026

Copy link
Copy Markdown
Author

@coderabbitai run pre-merge checks

@coderabbitai

coderabbitai Bot commented Mar 13, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Running Pre-merge checks. Results will be posted in the summary comment once complete.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/components/layout/Footer.jsx`:
- Around line 62-80: The measured iframe height can persist across src/mode
changes and prevent the fallback from applying for cross-origin content; update
the component to reset the iframe height whenever the iframe source or display
mode changes by adding a useEffect that watches the footer source/mode props and
calls setIframeHeight to the fallback (or null/0 whatever this component treats
as "use fallback"), and also consider setting the fallback inside
handleFooterFrameLoad's catch path using iframeRef and setIframeHeight so
cross-origin failures revert to the fallback; reference handleFooterFrameLoad,
iframeRef, and setIframeHeight when making this change.
- Around line 272-275: The footer is rendered via dangerouslySetInnerHTML using
footerValue (produced by normalizeFooterValue) which only trims and does not
sanitize; update the component to sanitize footerValue before assignment (or
inside normalizeFooterValue) using DOMPurify (import DOMPurify from 'dompurify')
and pass DOMPurify.sanitize(footerValue) to the dangerouslySetInnerHTML prop so
stored XSS cannot occur; ensure you only sanitize where
footer_html/status.footer_html is used and keep the symbol names:
normalizeFooterValue, footerValue, and the div with dangerouslySetInnerHTML.

In `@web/src/components/settings/OtherSetting.jsx`:
- Around line 389-392: The openGitHubRelease function currently calls
window.open(url, '_blank') which is vulnerable to reverse-tabnabbing; update the
call to include noopener and noreferrer (e.g. window.open(url, '_blank',
'noopener,noreferrer')) or, if you create an anchor, set rel="noopener
noreferrer", and additionally set newWindow.opener = null when available to be
extra-safe—modify the openGitHubRelease implementation accordingly.
- Around line 306-309: The parsed GitHub release HTML produced in the
OtherSetting component (the output of marked, e.g., the variable holding release
notes HTML that is later injected via dangerouslySetInnerHTML) is not sanitized
and creates an XSS risk; import DOMPurify and run the marked output through
DOMPurify.sanitize before storing it in state or passing it to
dangerouslySetInnerHTML (or, alternatively, configure marked to disable raw HTML
and still sanitize), then use the sanitized string when rendering to ensure
untrusted remote content is safe.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bf3a8ade-67b2-4bbd-949a-ce77bed21fa2

📥 Commits

Reviewing files that changed from the base of the PR and between 96af2a0 and b2bda46.

📒 Files selected for processing (3)
  • web/src/components/layout/Footer.jsx
  • web/src/components/settings/OtherSetting.jsx
  • web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.jsx

Comment on lines +62 to +80
const handleFooterFrameLoad = useCallback(() => {
try {
const iframeDocument = iframeRef.current?.contentDocument;
if (!iframeDocument) {
return;
}

const loadFooter = () => {
let footer_html = localStorage.getItem('footer_html');
if (footer_html) {
setFooter(footer_html);
const nextHeight = Math.max(
iframeDocument.documentElement?.scrollHeight || 0,
iframeDocument.body?.scrollHeight || 0,
);

if (nextHeight > 0) {
setIframeHeight(nextHeight);
}
} catch {
// Cross-origin iframe documents cannot be measured, so keep the fallback height.
}
};
}, []);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Reset iframe height when source/mode changes to keep fallback behavior correct.

If a previous same-origin footer set a large measured height, then a new cross-origin iframe can fail measurement and keep the old large height instead of the intended fallback.

🧩 Minimal fix
 import React, {
   useCallback,
   useContext,
+  useEffect,
   useMemo,
   useRef,
   useState,
 } from 'react';
@@
   const handleFooterFrameLoad = useCallback(() => {
@@
   }, []);
+
+  useEffect(() => {
+    if (footerRenderMode === 'iframe') {
+      setIframeHeight(240);
+    }
+  }, [footerRenderMode, footerValue]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleFooterFrameLoad = useCallback(() => {
try {
const iframeDocument = iframeRef.current?.contentDocument;
if (!iframeDocument) {
return;
}
const loadFooter = () => {
let footer_html = localStorage.getItem('footer_html');
if (footer_html) {
setFooter(footer_html);
const nextHeight = Math.max(
iframeDocument.documentElement?.scrollHeight || 0,
iframeDocument.body?.scrollHeight || 0,
);
if (nextHeight > 0) {
setIframeHeight(nextHeight);
}
} catch {
// Cross-origin iframe documents cannot be measured, so keep the fallback height.
}
};
}, []);
const handleFooterFrameLoad = useCallback(() => {
try {
const iframeDocument = iframeRef.current?.contentDocument;
if (!iframeDocument) {
return;
}
const nextHeight = Math.max(
iframeDocument.documentElement?.scrollHeight || 0,
iframeDocument.body?.scrollHeight || 0,
);
if (nextHeight > 0) {
setIframeHeight(nextHeight);
}
} catch {
// Cross-origin iframe documents cannot be measured, so keep the fallback height.
}
}, []);
useEffect(() => {
if (footerRenderMode === 'iframe') {
setIframeHeight(240);
}
}, [footerRenderMode, footerValue]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/layout/Footer.jsx` around lines 62 - 80, The measured
iframe height can persist across src/mode changes and prevent the fallback from
applying for cross-origin content; update the component to reset the iframe
height whenever the iframe source or display mode changes by adding a useEffect
that watches the footer source/mode props and calls setIframeHeight to the
fallback (or null/0 whatever this component treats as "use fallback"), and also
consider setting the fallback inside handleFooterFrameLoad's catch path using
iframeRef and setIframeHeight so cross-origin failures revert to the fallback;
reference handleFooterFrameLoad, iframeRef, and setIframeHeight when making this
change.

Comment on lines +272 to +275
<div
className='custom-footer'
dangerouslySetInnerHTML={{ __html: footerValue }}
></div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether footer content is currently sanitized before reaching dangerouslySetInnerHTML.
rg -n "dangerouslySetInnerHTML|normalizeFooterValue|sanitize|DOMPurify" web/src/components/layout/Footer.jsx web/src/helpers/footer.js

Repository: QuantumNous/new-api

Length of output: 514


🏁 Script executed:

cat -n web/src/helpers/footer.js

Repository: QuantumNous/new-api

Length of output: 1711


🏁 Script executed:

sed -n '45,60p' web/src/components/layout/Footer.jsx

Repository: QuantumNous/new-api

Length of output: 529


🏁 Script executed:

rg -l "dompurify|DOMPurify" web/

Repository: QuantumNous/new-api

Length of output: 75


🏁 Script executed:

fd package.json web/ | xargs grep -l "dompurify" 2>/dev/null || echo "Not found in direct grep"

Repository: QuantumNous/new-api

Length of output: 87


🏁 Script executed:

cat web/package.json | grep -A 5 -B 5 "dompurify" || echo "dompurify not in package.json"

Repository: QuantumNous/new-api

Length of output: 92


🏁 Script executed:

cat web/package.json | head -50

Repository: QuantumNous/new-api

Length of output: 1555


🏁 Script executed:

grep -A 10 "dompurify" web/bun.lock | head -20

Repository: QuantumNous/new-api

Length of output: 3988


🏁 Script executed:

rg -n "footer_html|getFooterHTML" web/src/ -A 2 -B 2

Repository: QuantumNous/new-api

Length of output: 2576


Sanitize footer HTML before rendering to avoid XSS.

footerValue is rendered with dangerouslySetInnerHTML without sanitization. The normalizeFooterValue() function only performs .trim()—it does not sanitize HTML. Since footer_html originates from server configuration (status.footer_html) and is persisted in localStorage, unsanitized rendering creates a stored XSS risk if this configuration is attacker-controlled or misconfigured by administrators.

Use DOMPurify (already available as a transitive dependency of mermaid) to sanitize the HTML before rendering:

Proposed fix
 import React, {
   useCallback,
   useContext,
   useMemo,
   useRef,
   useState,
 } from 'react';
+import DOMPurify from 'dompurify';
@@
   const footerRenderMode = useMemo(
     () => getFooterRenderMode(footerValue),
     [footerValue],
   );
+  const sanitizedFooterHtml = useMemo(
+    () => DOMPurify.sanitize(footerValue),
+    [footerValue],
+  );
@@
           ) : (
             <div
               className='custom-footer'
-              dangerouslySetInnerHTML={{ __html: footerValue }}
+              dangerouslySetInnerHTML={{ __html: sanitizedFooterHtml }}
             ></div>
           )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
className='custom-footer'
dangerouslySetInnerHTML={{ __html: footerValue }}
></div>
<div
className='custom-footer'
dangerouslySetInnerHTML={{ __html: sanitizedFooterHtml }}
></div>
🧰 Tools
🪛 Biome (2.4.6)

[error] 274-274: Avoid passing content using the dangerouslySetInnerHTML prop.

(lint/security/noDangerouslySetInnerHtml)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/layout/Footer.jsx` around lines 272 - 275, The footer is
rendered via dangerouslySetInnerHTML using footerValue (produced by
normalizeFooterValue) which only trims and does not sanitize; update the
component to sanitize footerValue before assignment (or inside
normalizeFooterValue) using DOMPurify (import DOMPurify from 'dompurify') and
pass DOMPurify.sanitize(footerValue) to the dangerouslySetInnerHTML prop so
stored XSS cannot occur; ensure you only sanitize where
footer_html/status.footer_html is used and keep the symbol names:
normalizeFooterValue, footerValue, and the div with dangerouslySetInnerHTML.

Comment on lines +306 to +309
/**
* 查询 GitHub 最新版本并在有更新时展示更新说明。
* @returns {Promise<void>}
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In marked v4.x, does marked.parse sanitize raw HTML by default, or is an external sanitizer (e.g., DOMPurify) required before using dangerouslySetInnerHTML?

💡 Result:

In marked v4.x, marked.parse() does not sanitize raw HTML by default—it will emit HTML as-is. If you’re rendering the result via React’s dangerouslySetInnerHTML, you should run an external HTML sanitizer (e.g., DOMPurify) on the output first. [1]

Also, marked’s old sanitize/sanitizer options were deprecated/removed, so relying on marked itself for sanitization isn’t the intended approach in v4.x. [2]

Sources:
[1] Marked docs (Security warning: “Marked does not sanitize the output HTML… DOMPurify recommended”)
[2] safe-marked / marked-sanitizer notes on marked not being safe-by-default and sanitize option deprecation/removal


🏁 Script executed:

wc -l web/src/components/settings/OtherSetting.jsx

Repository: QuantumNous/new-api

Length of output: 111


🏁 Script executed:

# Read the vulnerable code sections
sed -n '340,355p' web/src/components/settings/OtherSetting.jsx
cat -n web/src/components/settings/OtherSetting.jsx | sed -n '605,620p'

Repository: QuantumNous/new-api

Length of output: 880


🏁 Script executed:

# Check imports at the top of the file
head -30 web/src/components/settings/OtherSetting.jsx

Repository: QuantumNous/new-api

Length of output: 976


🏁 Script executed:

# Search for dangerouslySetInnerHTML usage in the file
grep -n "dangerouslySetInnerHTML" web/src/components/settings/OtherSetting.jsx

Repository: QuantumNous/new-api

Length of output: 143


🏁 Script executed:

# Check for marked import
grep -n "marked" web/src/components/settings/OtherSetting.jsx | head -5

Repository: QuantumNous/new-api

Length of output: 141


🏁 Script executed:

# Check for any existing DOMPurify usage
grep -n "DOMPurify" web/src/components/settings/OtherSetting.jsx

Repository: QuantumNous/new-api

Length of output: 45


Sanitize release markdown HTML before rendering.

Line 346 parses untrusted remote content (GitHub API response) and line 611 injects it via dangerouslySetInnerHTML. Since marked v4.x does not sanitize raw HTML by default, this creates an XSS vulnerability in the admin UI. Use DOMPurify to sanitize the parsed output before rendering.

🔒 Suggested fix
 import { marked } from 'marked';
+import DOMPurify from 'dompurify';

@@
       const { tag_name, body } = res;
@@
         setUpdateData({
           tag_name: tag_name,
-          content: marked.parse(body),
+          content: DOMPurify.sanitize(marked.parse(body ?? '')),
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/settings/OtherSetting.jsx` around lines 306 - 309, The
parsed GitHub release HTML produced in the OtherSetting component (the output of
marked, e.g., the variable holding release notes HTML that is later injected via
dangerouslySetInnerHTML) is not sanitized and creates an XSS risk; import
DOMPurify and run the marked output through DOMPurify.sanitize before storing it
in state or passing it to dangerouslySetInnerHTML (or, alternatively, configure
marked to disable raw HTML and still sanitize), then use the sanitized string
when rendering to ensure untrusted remote content is safe.

Comment on lines +389 to +392
/**
* 在新窗口中打开当前待更新版本的 GitHub Release 页面。
* @returns {void}
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify other _blank window.open call sites that may need the same hardening.
rg -nP "window\\.open\\([^\\n]*['_\"]_blank['\"]" web/src

Repository: QuantumNous/new-api

Length of output: 2068


🏁 Script executed:

fd "OtherSetting" web/src

Repository: QuantumNous/new-api

Length of output: 107


🏁 Script executed:

sed -n '385,400p' web/src/components/settings/OtherSetting.jsx

Repository: QuantumNous/new-api

Length of output: 361


🏁 Script executed:

sed -n '393,405p' web/src/components/settings/OtherSetting.jsx

Repository: QuantumNous/new-api

Length of output: 385


🏁 Script executed:

rg -n "window\.open" web/src/components/settings/OtherSetting.jsx

Repository: QuantumNous/new-api

Length of output: 83


Harden window.open usage against reverse-tabnabbing.

The openGitHubRelease function opens an external GitHub URL with window.open() using _blank without the noopener,noreferrer flags. Add these flags to prevent the opened page from accessing window.opener.

Suggested fix
const openGitHubRelease = () => {
  window.open(
    `https://github.com/Calcium-Ion/new-api/releases/tag/${updateData.tag_name}`,
    '_blank',
+   'noopener,noreferrer',
  );
};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* 在新窗口中打开当前待更新版本的 GitHub Release 页面。
* @returns {void}
*/
const openGitHubRelease = () => {
window.open(
`https://github.com/Calcium-Ion/new-api/releases/tag/${updateData.tag_name}`,
'_blank',
'noopener,noreferrer',
);
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/settings/OtherSetting.jsx` around lines 389 - 392, The
openGitHubRelease function currently calls window.open(url, '_blank') which is
vulnerable to reverse-tabnabbing; update the call to include noopener and
noreferrer (e.g. window.open(url, '_blank', 'noopener,noreferrer')) or, if you
create an anchor, set rel="noopener noreferrer", and additionally set
newWindow.opener = null when available to be extra-safe—modify the
openGitHubRelease implementation accordingly.

@asdv23 asdv23 closed this Mar 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant