From 47a99c4f7d762307e9557f2d553a329782fb8a08 Mon Sep 17 00:00:00 2001 From: Gandalf Date: Thu, 11 Dec 2025 01:08:55 +0100 Subject: [PATCH] Add post context to content-renderer sanitization warnings Include author and permlink in renderer warning messages to help identify which posts trigger blocked iframe/image warnings. Previously warnings like "Blocked image (invalid src)" or "Blocked iframe (not whitelisted)" gave no indication of which post content caused the issue. Changes: - Add PostContext interface with optional author/permlink fields - Update TagTransformingSanitizer.sanitize() to accept PostContext - Update warning messages to include context (e.g., "in @author/permlink") - Export PostContext type from renderer package - Update DefaultRenderer.render() to pass context through - Update RendererContainer component to pass author/permlink - Update content.tsx and comment-list-item.tsx call sites Closes #763 --- .../app/[param]/[p2]/[permlink]/content.tsx | 1 + .../post-rendering/comment-list-item.tsx | 1 + .../post-rendering/rendererContainer.tsx | 9 ++++++-- packages/renderer/src/index.ts | 1 + .../src/renderers/default/DefaultRenderer.ts | 17 ++++++++------ .../sanitization/TagTransformingSanitizer.ts | 22 ++++++++++++++++--- 6 files changed, 39 insertions(+), 12 deletions(-) diff --git a/apps/blog/app/[param]/[p2]/[permlink]/content.tsx b/apps/blog/app/[param]/[p2]/[permlink]/content.tsx index 89e37f170..afc33ddb1 100644 --- a/apps/blog/app/[param]/[p2]/[permlink]/content.tsx +++ b/apps/blog/app/[param]/[p2]/[permlink]/content.tsx @@ -362,6 +362,7 @@ const PostContent = () => { mainPost={postData.depth === 0} body={crossPostData?.body ?? postData.body} author={postData.author} + permlink={postData.permlink} className={postClassName} /> diff --git a/apps/blog/features/post-rendering/comment-list-item.tsx b/apps/blog/features/post-rendering/comment-list-item.tsx index a085f6d38..ac02bcb7b 100644 --- a/apps/blog/features/post-rendering/comment-list-item.tsx +++ b/apps/blog/features/post-rendering/comment-list-item.tsx @@ -329,6 +329,7 @@ const CommentListItem = ({ diff --git a/apps/blog/features/post-rendering/rendererContainer.tsx b/apps/blog/features/post-rendering/rendererContainer.tsx index adf27b701..150ab1d20 100644 --- a/apps/blog/features/post-rendering/rendererContainer.tsx +++ b/apps/blog/features/post-rendering/rendererContainer.tsx @@ -11,6 +11,7 @@ import { isUrlWhitelisted } from '@hive/ui/config/lists/phishing'; const RendererContainer = ({ body, author, + permlink, dataTestid, communityDescription, mainPost, @@ -18,6 +19,7 @@ const RendererContainer = ({ }: { body: string; author: string; + permlink?: string; dataTestid?: string; communityDescription?: boolean; className?: string; @@ -82,8 +84,11 @@ const RendererContainer = ({ }, [body, hiveRenderer]); const htmlBody = useMemo(() => { - if (body) return hiveRenderer.render(body); - }, [hiveRenderer, body]); + if (body) { + const postContext = author || permlink ? { author, permlink } : undefined; + return hiveRenderer.render(body, postContext); + } + }, [hiveRenderer, body, author, permlink]); return !htmlBody ? ( diff --git a/packages/renderer/src/index.ts b/packages/renderer/src/index.ts index 92643a7cf..4c067a904 100644 --- a/packages/renderer/src/index.ts +++ b/packages/renderer/src/index.ts @@ -8,6 +8,7 @@ export {TwitterPlugin} from './renderers/default/plugins/TwitterPlugin'; export {InstagramPlugin} from './renderers/default/plugins/InstagramPlugin'; export {TablePlugin} from './renderers/default/plugins/TablePlugin'; export type {RendererPlugin} from './renderers/default/plugins/RendererPlugin'; +export type {PostContext} from './renderers/default/sanitization/TagTransformingSanitizer'; export const HiveContentRenderer = { DefaultRenderer, diff --git a/packages/renderer/src/renderers/default/DefaultRenderer.ts b/packages/renderer/src/renderers/default/DefaultRenderer.ts index eb04654c0..ae3e3b977 100644 --- a/packages/renderer/src/renderers/default/DefaultRenderer.ts +++ b/packages/renderer/src/renderers/default/DefaultRenderer.ts @@ -6,7 +6,7 @@ import {Localization, LocalizationOptions} from './Localization'; import type {RendererPlugin} from './plugins/RendererPlugin'; import remarkableSpoiler from './plugins/SpoilerPlugin'; import {PreliminarySanitizer} from './sanitization/PreliminarySanitizer'; -import {TagTransformingSanitizer} from './sanitization/TagTransformingSanitizer'; +import {TagTransformingSanitizer, PostContext} from './sanitization/TagTransformingSanitizer'; /** * DefaultRenderer is a configurable HTML/Markdown renderer that provides: @@ -68,22 +68,24 @@ export class DefaultRenderer { /** * Renders the input text to HTML * @param input - Markdown or HTML text to render + * @param postContext - Optional context about the post (author/permlink) for logging * @returns Rendered and processed HTML * @throws Will throw if input is empty or invalid */ - public render(input: string): string { + public render(input: string, postContext?: PostContext): string { // Validate input ow(input, 'input', ow.string.nonEmpty); - return this.doRender(input); + return this.doRender(input, postContext); } /** * Renders the input text to HTML with a specific locale * @param text - Markdown or HTML text to render + * @param postContext - Optional context about the post for logging * @returns Rendered and processed HTML * @throws Will throw if input is empty or invalid */ - private doRender(text: string): string { + private doRender(text: string, postContext?: PostContext): string { // Pre-process with plugins text = this.runPluginPhase('preProcess', text); // Preliminary sanitization @@ -98,7 +100,7 @@ export class DefaultRenderer { // Parse the HTML and sanitize it text = this.domParser.parse(text).getParsedDocumentAsString(); // Check for script tags and other security issues - text = this.sanitize(text); + text = this.sanitize(text, postContext); // Check for security issues SecurityChecker.checkSecurity(text, {allowScriptTag: this.options.allowInsecureScriptTags}); // Embed assets and resize them @@ -183,6 +185,7 @@ export class DefaultRenderer { /** * Sanitizes the HTML text by removing potentially harmful content * @param text - The HTML text to sanitize + * @param postContext - Optional context about the post for logging * @returns Sanitized HTML text * @remarks * This method can be skipped if skipSanitization option is set to true. @@ -191,12 +194,12 @@ export class DefaultRenderer { * - Transform certain tags according to renderer options * - Apply security policies to links and embedded content */ - private sanitize(text: string): string { + private sanitize(text: string, postContext?: PostContext): string { if (this.options.skipSanitization) { return text; } - return this.tagTransformingSanitizer.sanitize(text); + return this.tagTransformingSanitizer.sanitize(text, postContext); } /** * Validates the renderer options diff --git a/packages/renderer/src/renderers/default/sanitization/TagTransformingSanitizer.ts b/packages/renderer/src/renderers/default/sanitization/TagTransformingSanitizer.ts index 2e7ec9b99..6b6675447 100644 --- a/packages/renderer/src/renderers/default/sanitization/TagTransformingSanitizer.ts +++ b/packages/renderer/src/renderers/default/sanitization/TagTransformingSanitizer.ts @@ -11,6 +11,7 @@ export class TagTransformingSanitizer { private options: TagsSanitizerOptions; private localization: LocalizationOptions; private sanitizationErrors: string[] = []; + private currentPostContext?: PostContext; public constructor(options: TagsSanitizerOptions, localization: LocalizationOptions) { this.validate(options); @@ -25,12 +26,22 @@ export class TagTransformingSanitizer { * Uses the sanitize-html library with custom configuration for tag transformation. * * @param text - The HTML content to sanitize + * @param postContext - Optional context about the post being rendered (for logging) * @returns A sanitized version of the HTML content with transformed tags and removed unsafe content */ - public sanitize(text: string): string { + public sanitize(text: string, postContext?: PostContext): string { + this.currentPostContext = postContext; return sanitize(text, this.generateSanitizeConfig()); } + private formatPostContext(): string { + if (!this.currentPostContext) return ''; + const { author, permlink } = this.currentPostContext; + if (author && permlink) return ` in @${author}/${permlink}`; + if (author) return ` by @${author}`; + return ''; + } + public getErrors(): string[] { return this.sanitizationErrors; } @@ -98,7 +109,7 @@ export class TagTransformingSanitizer { return iframeToBeReturned; } } - Log.log().warn('Blocked, did not match iframe "src" white list urls:', tagName, attributes); + Log.log().warn(`Blocked iframe (not whitelisted)${this.formatPostContext()}: src="${srcAtty || '(empty)'}"`); this.sanitizationErrors.push('Invalid iframe URL: ' + srcAtty); const retTag: sanitize.Tag = {tagName: 'div', text: `(Unsupported ${srcAtty})`, attribs: {}}; @@ -117,7 +128,7 @@ export class TagTransformingSanitizer { const {src, alt} = attribs; // eslint-disable-next-line security/detect-unsafe-regex if (!/^(https?:)?\/\//i.test(src)) { - Log.log().warn('Blocked, image tag src does not appear to be a url', tagName, attribs); + Log.log().warn(`Blocked image (invalid src)${this.formatPostContext()}: src="${src || '(empty)'}"`); this.sanitizationErrors.push('An image in this post did not save properly.'); const retTagOnNoUrl: sanitize.Tag = { tagName: 'img', @@ -228,3 +239,8 @@ export interface TagsSanitizerOptions { isLinkSafeFn: (url: string) => boolean; addExternalCssClassToMatchingLinksFn: (url: string) => boolean; } + +export interface PostContext { + author?: string; + permlink?: string; +} -- GitLab