Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/paywall/src/browser/entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createRoot } from "react-dom/client";
import type {} from "./window";
import { StellarPaywall } from "./StellarPaywall";
import { validateX402Config } from "./validate";
import { sanitizeHTML } from "./sanitize";

// Stellar-specific paywall entry point
window.addEventListener("load", () => {
Expand Down Expand Up @@ -42,7 +43,7 @@ window.addEventListener("load", () => {
onSuccessfulResponse={async (response: Response) => {
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("text/html")) {
document.documentElement.innerHTML = await response.text();
document.documentElement.innerHTML = sanitizeHTML(await response.text());
} else {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
Expand Down
26 changes: 26 additions & 0 deletions packages/paywall/src/browser/sanitize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const EVENT_HANDLER_RE = /^on/i;

/**
* Parse an HTML string with DOMParser and strip dangerous content
* (script/iframe/object/embed elements and inline event-handler attributes)
* before it is injected into the document via innerHTML.
*/
export function sanitizeHTML(raw: string): string {
const doc = new DOMParser().parseFromString(raw, "text/html");
Comment on lines +8 to +9
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

src/browser is excluded from this package’s tsc typecheck and ESLint runs, so this new sanitizer logic won’t be statically checked in CI via the existing scripts. Given this is security-sensitive code, it would be safer to add a dedicated browser tsconfig/typecheck step (or include these files) so regressions (e.g., DOM API misuse) are caught early.

Copilot uses AI. Check for mistakes.

const dangerous = doc.querySelectorAll("script, iframe, object, embed");
for (const el of dangerous) {
el.remove();
}

const all = doc.querySelectorAll("*");
for (const el of all) {
for (const attr of [...el.attributes]) {
if (EVENT_HANDLER_RE.test(attr.name)) {
el.removeAttribute(attr.name);
}
}
}

return doc.documentElement.innerHTML;
Comment on lines +1 to +25
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The current sanitizer only removes a small set of elements and strips on* attributes, but it still allows common XSS vectors such as href="javascript:..." / xlink:href, src="javascript:...", formaction/action, srcset with javascript:/data: payloads, and <meta http-equiv="refresh"> redirects. This means the response can still execute script via user interaction or browser behaviors even after sanitization. Consider switching to a well-maintained allowlist sanitizer (e.g., DOMPurify with an explicit config) or, at minimum, additionally strip/validate URL-bearing attributes to disallow dangerous protocols and remove other hazardous elements like meta[http-equiv] and base.

Suggested change
const EVENT_HANDLER_RE = /^on/i;
/**
* Parse an HTML string with DOMParser and strip dangerous content
* (script/iframe/object/embed elements and inline event-handler attributes)
* before it is injected into the document via innerHTML.
*/
export function sanitizeHTML(raw: string): string {
const doc = new DOMParser().parseFromString(raw, "text/html");
const dangerous = doc.querySelectorAll("script, iframe, object, embed");
for (const el of dangerous) {
el.remove();
}
const all = doc.querySelectorAll("*");
for (const el of all) {
for (const attr of [...el.attributes]) {
if (EVENT_HANDLER_RE.test(attr.name)) {
el.removeAttribute(attr.name);
}
}
}
return doc.documentElement.innerHTML;
import DOMPurify from "dompurify";
/**
* Sanitize an HTML string before it is injected into the document via innerHTML.
*
* Uses DOMPurify with an explicit configuration to strip dangerous elements,
* event-handler attributes, and unsafe URL-bearing attributes/protocols.
*/
export function sanitizeHTML(raw: string): string {
return DOMPurify.sanitize(raw, {
// Explicitly forbid high-risk elements that can change page behavior or
// introduce script execution or redirects.
FORBID_TAGS: ["script", "iframe", "object", "embed", "meta", "base"],
// Forbid particularly risky attributes; DOMPurify also strips `on*` handlers
// and unsafe URL protocols (e.g. `javascript:`) by default.
FORBID_ATTR: ["xlink:href", "formaction"],
});

Copilot uses AI. Check for mistakes.
}
Loading