Skip to content
Merged
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
16 changes: 15 additions & 1 deletion packages/md-react-preview/app/src/preview-block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,17 @@ export function PreviewBlock({
const initialColorScheme = useRef(colorScheme);
useEffect(() => {
if (colorScheme === initialColorScheme.current) return;
iframeRef.current?.contentWindow?.postMessage({ type: "mrp-theme", theme: colorScheme }, "*");
// Security: specify origin instead of "*" to restrict postMessage recipients
iframeRef.current?.contentWindow?.postMessage(
{ type: "mrp-theme", theme: colorScheme },
window.location.origin,
);
}, [colorScheme]);

useEffect(() => {
function onMessage(e: MessageEvent) {
// Security: validate postMessage origin to prevent cross-origin message spoofing
if (e.origin !== window.location.origin) return;
if (e.data?.type === "mrp-resize" && e.data?.blockId === blockId) {
setIframeHeight(e.data.height);
}
Expand Down Expand Up @@ -198,6 +204,14 @@ export function PreviewBlock({
>
<ExternalLinkIcon />
</a>
{/*
Security note: no sandbox attribute is set on this iframe.
Preview blocks are authored by trusted developers (markdown authors),
and adding sandbox="allow-scripts" alone would break ES module loading
(CORS) and postMessage origin checks. Adding both allow-scripts and
allow-same-origin together provides no real security benefit for
same-origin iframes.
*/}
<iframe
ref={iframeRef}
src={previewUrl}
Expand Down
2 changes: 1 addition & 1 deletion packages/md-react-preview/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@izumisy/md-react-preview",
"version": "0.1.0",
"version": "0.1.1",
"description": "md-react-preview — component previewer powered by MDX",
"bin": {
"mrp": "./dist/cli.mjs"
Expand Down
11 changes: 10 additions & 1 deletion packages/md-react-preview/src/preview-transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,16 @@ export function extractPreviewBlocks(source: string): PreviewBlock[] {
}

export function escapeJsString(s: string): string {
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
return (
s
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"')
.replace(/\n/g, "\\n")
// Security: escape characters that could break out of JS string literals
.replace(/\r/g, "\\r")
.replace(/\u2028/g, "\\u2028")
.replace(/\u2029/g, "\\u2029")
);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/vite-plugin-react-preview/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@izumisy/vite-plugin-react-preview",
"version": "0.1.0",
"version": "0.1.1",
"description": "Vite plugin for rendering React component previews in iframe",
"type": "module",
"exports": {
Expand Down
5 changes: 4 additions & 1 deletion packages/vite-plugin-react-preview/src/preview-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,9 @@ function applyTheme(theme) {
if (themeParam === "dark" || themeParam === "light") {
applyTheme(themeParam);
}
// Security: validate postMessage origin to prevent cross-origin message spoofing
window.addEventListener("message", function(e) {
if (e.origin !== location.origin) return;
if (e.data && e.data.type === "mrp-theme" && (e.data.theme === "dark" || e.data.theme === "light")) {
applyTheme(e.data.theme);
}
Expand All @@ -146,9 +148,10 @@ registry[blockId]().then(function(mod) {
createRoot(root).render(createElement(mod.default));

new ResizeObserver(function() {
// Security: specify origin instead of "*" to restrict postMessage recipients
window.parent.postMessage(
{ type: "mrp-resize", blockId: blockId, height: root.scrollHeight },
"*"
location.origin
);
}).observe(root);
});
Expand Down
2 changes: 1 addition & 1 deletion packages/vitepress-plugin-react-preview/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@izumisy/vitepress-plugin-react-preview",
"version": "0.1.0",
"version": "0.1.1",
"description": "VitePress plugin for rendering React component previews via iframe",
"type": "module",
"exports": {
Expand Down
13 changes: 12 additions & 1 deletion packages/vitepress-plugin-react-preview/src/PreviewBlock.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,17 @@ let themeObserver: MutationObserver | null = null;
function syncThemeToIframe() {
const iframe = iframeRef.value;
if (iframe?.contentWindow) {
// Security: specify origin instead of "*" to restrict postMessage recipients
iframe.contentWindow.postMessage(
{ type: "mrp-theme", theme: currentTheme.value },
"*"
window.location.origin
);
}
}

function onMessage(e: MessageEvent) {
// Security: validate postMessage origin to prevent cross-origin message spoofing
if (e.origin !== window.location.origin) return;
if (
e.data?.type === "mrp-resize" &&
e.data?.blockId === props.blockId
Expand Down Expand Up @@ -112,6 +115,14 @@ onBeforeUnmount(() => {
</template>
<template v-else>
<div class="mrp-preview-render">
<!--
Security note: no sandbox attribute is set on this iframe.
Preview blocks are authored by trusted developers (markdown authors),
and adding sandbox="allow-scripts" alone would break ES module loading
(CORS) and postMessage origin checks. Adding both allow-scripts and
allow-same-origin together provides no real security benefit for
same-origin iframes.
-->
<iframe
ref="iframeRef"
:src="previewUrl"
Expand Down
Loading