mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-13 14:37:26 +00:00
Sanitize rendered web content (#77)
This commit is contained in:
parent
cb192906bd
commit
59013cf0af
6 changed files with 167 additions and 133 deletions
19
apps/logicsrc-web/contract/html-sanitizer.test.ts
Normal file
19
apps/logicsrc-web/contract/html-sanitizer.test.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { sanitizeRenderedHtml } from "@/lib/html";
|
||||
|
||||
describe("sanitizeRenderedHtml", () => {
|
||||
it("removes script tags, event handlers, and javascript URLs", () => {
|
||||
const html = sanitizeRenderedHtml(
|
||||
'<h2>Title</h2><p>Hello</p><script>alert(1)</script><img src="https://example.com/a.png" onerror="alert(1)"><a href="javascript:alert(1)">bad</a>'
|
||||
);
|
||||
|
||||
expect(html).toContain("<h2>Title</h2>");
|
||||
expect(html).toContain("<p>Hello</p>");
|
||||
expect(html).toContain('<img src="https://example.com/a.png" />');
|
||||
expect(html).toContain("<a>bad</a>");
|
||||
expect(html).not.toContain("<script>");
|
||||
expect(html).not.toContain("onerror");
|
||||
expect(html).not.toContain("javascript:");
|
||||
});
|
||||
});
|
||||
|
|
@ -17,13 +17,15 @@
|
|||
"marked": "^18.0.5",
|
||||
"next": "16.2.6",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0"
|
||||
"react-dom": "19.2.0",
|
||||
"sanitize-html": "^2.17.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.2.0",
|
||||
"@types/sanitize-html": "^2.16.1",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.8"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type { ReactNode } from "react";
|
|||
import type { Metadata } from "next";
|
||||
import { publicClient } from "@/lib/supabase";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { sanitizeRenderedHtml } from "@/lib/html";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
|
|
@ -84,6 +85,7 @@ export default async function BlogPostPage({
|
|||
mainEntityOfPage: `${SITE_URL}/blog/${post.slug}`,
|
||||
publisher: { "@id": `${SITE_URL}/#organization` },
|
||||
};
|
||||
const html = sanitizeRenderedHtml(post.html);
|
||||
|
||||
return (
|
||||
<SiteShell active="Blog">
|
||||
|
|
@ -114,7 +116,7 @@ export default async function BlogPostPage({
|
|||
<div
|
||||
className="blog-content"
|
||||
style={{ lineHeight: 1.7 }}
|
||||
dangerouslySetInnerHTML={{ __html: post.html }}
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
</article>
|
||||
</SiteShell>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type { Metadata } from "next";
|
|||
import { marked } from "marked";
|
||||
import { DOC_SLUGS, docExcerpt, docTitle, readDoc } from "@/lib/docs";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { sanitizeRenderedHtml } from "@/lib/html";
|
||||
|
||||
// Statically generate one page per curated doc at build time.
|
||||
export function generateStaticParams(): Array<{ slug: string }> {
|
||||
|
|
@ -37,7 +38,8 @@ export default async function DocPage({
|
|||
const md = readDoc(slug);
|
||||
if (!md) notFound();
|
||||
|
||||
const html = await marked.parse(md);
|
||||
const rawHtml = await marked.parse(md);
|
||||
const html = sanitizeRenderedHtml(rawHtml);
|
||||
|
||||
return (
|
||||
<SiteShell active="Docs">
|
||||
|
|
|
|||
33
apps/logicsrc-web/src/lib/html.ts
Normal file
33
apps/logicsrc-web/src/lib/html.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import sanitizeHtml from "sanitize-html";
|
||||
|
||||
const allowedTags = sanitizeHtml.defaults.allowedTags.concat([
|
||||
"figure",
|
||||
"figcaption",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"img"
|
||||
]);
|
||||
|
||||
const allowedAttributes = {
|
||||
...sanitizeHtml.defaults.allowedAttributes,
|
||||
a: ["href", "name", "target", "rel"],
|
||||
img: ["src", "alt", "title", "width", "height", "loading"],
|
||||
code: ["class"],
|
||||
pre: ["class"]
|
||||
};
|
||||
|
||||
export function sanitizeRenderedHtml(html: string): string {
|
||||
return sanitizeHtml(html, {
|
||||
allowedTags,
|
||||
allowedAttributes,
|
||||
allowedSchemes: ["http", "https", "mailto", "tel"],
|
||||
allowedSchemesByTag: {
|
||||
img: ["http", "https"]
|
||||
},
|
||||
allowProtocolRelative: false
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue