diff --git a/.env.example b/.env.example
index 08e5133e05..38cefc49ca 100644
--- a/.env.example
+++ b/.env.example
@@ -37,6 +37,11 @@ JWT_SECRET=your-secret-key-change-in-production
# Other Configuration
NEXT_PUBLIC_SIGNUP_URL=https://signup.steemit.com
+# Image proxy prefix. Post-body images are proxied through this host as
+# /p/:base58?width=&mode=fit&format=match (mirrors master's
+# $STM_Config.img_proxy_prefix). Default is the production CDN.
+# NEXT_PUBLIC_IMAGE_PROXY_PREFIX=https://steemitimages.com/
+
# External Steemit wallet (Condenser only links out). Production default: https://steemitwallet.com
# Unset in production build to use that default; set for test/staging, e.g.:
# NEXT_PUBLIC_WALLET_URL=https://wallet.steemitdev.com
diff --git a/components/elements/MarkdownViewer.tsx b/components/elements/MarkdownViewer.tsx
index 86f45a8c92..76cdee302e 100644
--- a/components/elements/MarkdownViewer.tsx
+++ b/components/elements/MarkdownViewer.tsx
@@ -1,6 +1,10 @@
'use client';
-import { useEffect, useRef } from 'react';
+import { useMemo } from 'react';
+import MarkdownIt from 'markdown-it';
+import sanitizeHtml from 'sanitize-html';
+import htmlReady from '@/lib/html-ready';
+import sanitizeConfig from '@/lib/sanitize-config';
interface MarkdownViewerProps {
text: string;
@@ -9,11 +13,28 @@ interface MarkdownViewerProps {
hideImages?: boolean;
}
+// Markdown→HTML renderer. html:true so author HTML passes through to sanitize;
+// breaks:true so single newlines render as
(Steemit's markdown dialect).
+// Uses markdown-it (actively maintained, CommonMark-spec) — replaces the
+// unmaintained remarkable, with an identical options surface.
+const md = new MarkdownIt({
+ html: true,
+ breaks: true,
+ linkify: false,
+ typographer: false,
+ quotes: '“”‘’',
+});
+
/**
- * MarkdownViewer component
- * Renders markdown content as HTML
- * Simplified version migrated from legacy/src/app/components/cards/MarkdownViewer.jsx
- * TODO: Add full markdown rendering with remarkable, HtmlReady, and sanitize-html
+ * MarkdownViewer — renders post-body markdown as sanitized, proxied HTML.
+ *
+ * Pipeline (order is security-critical):
+ * 1. markdown-it: markdown → HTML
+ * 2. HtmlReady: URL normalization + link/mention/#tag linkify + image proxy
+ * 3. sanitize-html (SanitizeConfig): XSS filter — allowedTags/Attributes,
+ * iframe whitelist, link/image hardening.
+ *
+ * Ported from master's MarkdownViewer.jsx, adapted to the next branch.
*/
export default function MarkdownViewer({
text,
@@ -21,54 +42,56 @@ export default function MarkdownViewer({
large = false,
hideImages = false,
}: MarkdownViewerProps) {
- const containerRef = useRef$1
')
- .replace(/^## (.*$)/gim, '$1
')
- .replace(/^# (.*$)/gim, '$1
')
- // Bold
- .replace(/\*\*(.*?)\*\*/gim, '$1')
- .replace(/__(.*?)__/gim, '$1')
- // Italic
- .replace(/\*(.*?)\*/gim, '$1')
- .replace(/_(.*?)_/gim, '$1')
- // Links
- .replace(/\[([^\]]+)\]\(([^)]+)\)/gim, '$1')
- // Line breaks
- .replace(/\n\n/gim, '
')
- .replace(/\n/gim, '
');
+ let body = text;
- // Wrap in paragraph if not already wrapped
- if (!html.startsWith('<')) {
- html = `
${html}
`; + // Detect raw-HTML posts (wrapped in … or a leading). + let isHtml = false; + const m = body.match(/^([\S\s]*)<\/html>$/); + if (m && m.length === 2) { + isHtml = true; + body = m[1]; + } else if (/^
[\S\s]*<\/p>/.test(body)) {
+ isHtml = true;
}
- // Handle images
- if (hideImages) {
- html = html.replace(/]*>/gi, (match) => {
- const srcMatch = match.match(/src="([^"]+)"/);
- return srcMatch ? `[Image: ${srcMatch[1]}]` : match;
- });
+ // Strip HTML comments ("JS-DOS" mitigation).
+ body = body.replace(/|$)/g, '(html comment removed: $1)');
+
+ // 1. markdown → HTML (skip for raw-HTML posts).
+ let rendered = isHtml ? body : md.render(body);
+
+ // 2. HtmlReady mutation (linkify, proxify images, wrap iframes, …).
+ rendered = htmlReady(rendered, { hideImages }).html;
+
+ // 3. sanitize-html (XSS filter + iframe whitelist).
+ const sanitizeErrors: string[] = [];
+ const clean = sanitizeHtml(
+ rendered,
+ sanitizeConfig({ large, highQualityPost: true, sanitizeErrors })
+ );
+
+ if (sanitizeErrors.length > 0) {
+ console.warn('MarkdownViewer sanitize errors:', sanitizeErrors);
}
- containerRef.current.innerHTML = html;
- }, [text, hideImages]);
+ // Secondary trap: refuse to render any