mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
refactor(api): 📦 Change sanitizer from DOMPurify to xss
This commit is contained in:
parent
a430db5c30
commit
154f17ab12
|
|
@ -555,14 +555,14 @@ export const contentToHtml = async (
|
|||
let htmlContent: string;
|
||||
|
||||
if (content["text/html"]) {
|
||||
htmlContent = content["text/html"].content;
|
||||
htmlContent = await sanitizeHtml(content["text/html"].content);
|
||||
} else if (content["text/markdown"]) {
|
||||
htmlContent = await sanitizeHtml(
|
||||
await markdownParse(content["text/markdown"].content),
|
||||
);
|
||||
} else if (content["text/plain"]?.content) {
|
||||
// Split by newline and add <p> tags
|
||||
htmlContent = content["text/plain"].content
|
||||
htmlContent = (await sanitizeHtml(content["text/plain"].content))
|
||||
.split("\n")
|
||||
.map((line) => `<p>${line}</p>`)
|
||||
.join("\n");
|
||||
|
|
|
|||
|
|
@ -77,10 +77,8 @@
|
|||
"cli-parser": "workspace:*",
|
||||
"cli-table": "^0.3.11",
|
||||
"config-manager": "workspace:*",
|
||||
"dompurify": "^3.1.2",
|
||||
"drizzle-orm": "^0.30.7",
|
||||
"extract-zip": "^2.0.1",
|
||||
"happy-dom": "14.7.1",
|
||||
"html-to-text": "^9.0.5",
|
||||
"ioredis": "^5.3.2",
|
||||
"ip-matching": "^2.1.2",
|
||||
|
|
@ -103,6 +101,8 @@
|
|||
"request-parser": "workspace:*",
|
||||
"sharp": "^0.33.3",
|
||||
"string-comparison": "^1.3.0",
|
||||
"stringify-entities": "^4.0.4",
|
||||
"xss": "^1.0.15",
|
||||
"zod": "^3.22.4",
|
||||
"zod-validation-error": "^3.2.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import { config } from "~packages/config-manager";
|
|||
import type { Attachment as APIAttachment } from "~types/mastodon/attachment";
|
||||
import type { Status as APIStatus } from "~types/mastodon/status";
|
||||
import { User } from "./user";
|
||||
import { sanitizedHtmlStrip } from "@sanitization";
|
||||
|
||||
/**
|
||||
* Gives helpers to fetch notes from database in a nice format
|
||||
|
|
@ -208,7 +209,7 @@ export class Note {
|
|||
contentType: "text/html",
|
||||
visibility,
|
||||
sensitive: is_sensitive,
|
||||
spoilerText: spoiler_text,
|
||||
spoilerText: await sanitizedHtmlStrip(spoiler_text),
|
||||
uri: uri || null,
|
||||
replyId: replyId ?? null,
|
||||
quotingId: quoteId ?? null,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { sanitizeHtml } from "@sanitization";
|
||||
import { sanitizeHtml, sanitizedHtmlStrip } from "@sanitization";
|
||||
import { config } from "config-manager";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import ISO6391 from "iso-639-1";
|
||||
|
|
@ -97,10 +97,9 @@ export default apiRoute<typeof meta, typeof schema>(
|
|||
|
||||
const sanitizedNote = await sanitizeHtml(note ?? "");
|
||||
|
||||
const sanitizedDisplayName = await sanitizeHtml(display_name ?? "", {
|
||||
ALLOWED_TAGS: [],
|
||||
ALLOWED_ATTR: [],
|
||||
});
|
||||
const sanitizedDisplayName = await sanitizedHtmlStrip(
|
||||
display_name ?? "",
|
||||
);
|
||||
|
||||
let mediaManager: MediaBackend;
|
||||
|
||||
|
|
|
|||
|
|
@ -361,4 +361,62 @@ describe(meta.route, () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTML injection testing", () => {
|
||||
test("should not allow HTML injection", async () => {
|
||||
const response = await sendTestRequest(
|
||||
new Request(new URL(meta.route, config.http.base_url), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${tokens[0].accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
status: "Hi! <script>alert('Hello, world!');</script>",
|
||||
federate: false,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toBe(
|
||||
"application/json",
|
||||
);
|
||||
|
||||
const object = (await response.json()) as APIStatus;
|
||||
|
||||
expect(object.content).toBe(
|
||||
"<p>Hi! <script>alert('Hello, world!');</script></p>",
|
||||
);
|
||||
});
|
||||
|
||||
test("should not allow HTML injection in spoiler_text", async () => {
|
||||
const response = await sendTestRequest(
|
||||
new Request(new URL(meta.route, config.http.base_url), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${tokens[0].accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
status: "Hello, world!",
|
||||
spoiler_text:
|
||||
"uwu <script>alert('Hello, world!');</script>",
|
||||
federate: false,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toBe(
|
||||
"application/json",
|
||||
);
|
||||
|
||||
const object = (await response.json()) as APIStatus;
|
||||
|
||||
expect(object.spoiler_text).toBe(
|
||||
"uwu <script>alert('Hello, world!');</script>",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,52 +1,41 @@
|
|||
import { config } from "config-manager";
|
||||
import DOMPurify from "dompurify";
|
||||
import { GlobalWindow } from "happy-dom";
|
||||
import xss, { type IFilterXSSOptions } from "xss";
|
||||
import { stringifyEntitiesLight } from "stringify-entities";
|
||||
|
||||
const window = new GlobalWindow();
|
||||
export const sanitizedHtmlStrip = (html: string) => {
|
||||
return sanitizeHtml(html, {
|
||||
whiteList: {},
|
||||
});
|
||||
};
|
||||
|
||||
export const sanitizeHtml = async (
|
||||
html: string,
|
||||
extraConfig?: DOMPurify.Config,
|
||||
extraConfig?: IFilterXSSOptions,
|
||||
) => {
|
||||
// @ts-expect-error Types clash but it works i swear
|
||||
const sanitizedHtml = DOMPurify(window).sanitize(html, {
|
||||
ALLOWED_TAGS: [
|
||||
"a",
|
||||
"p",
|
||||
"br",
|
||||
"b",
|
||||
"i",
|
||||
"em",
|
||||
"strong",
|
||||
"del",
|
||||
"code",
|
||||
"u",
|
||||
"pre",
|
||||
"ul",
|
||||
"ol",
|
||||
"li",
|
||||
"blockquote",
|
||||
],
|
||||
ALLOWED_ATTR: [
|
||||
"href",
|
||||
"target",
|
||||
"title",
|
||||
"rel",
|
||||
"class",
|
||||
"start",
|
||||
"reversed",
|
||||
"value",
|
||||
],
|
||||
ALLOWED_URI_REGEXP: new RegExp(
|
||||
`/^(?:(?:${config.validation.url_scheme_whitelist.join(
|
||||
"|",
|
||||
)}):|[^a-z]|[a-z+.-]+(?:[^a-z+.-:]|$))/i`,
|
||||
),
|
||||
USE_PROFILES: {
|
||||
mathMl: true,
|
||||
const sanitizedHtml = xss(html, {
|
||||
whiteList: {
|
||||
a: ["href", "title", "target", "rel", "class"],
|
||||
p: ["class"],
|
||||
br: ["class"],
|
||||
b: ["class"],
|
||||
i: ["class"],
|
||||
em: ["class"],
|
||||
strong: ["class"],
|
||||
del: ["class"],
|
||||
code: ["class"],
|
||||
u: ["class"],
|
||||
pre: ["class"],
|
||||
ul: ["class"],
|
||||
ol: ["class"],
|
||||
li: ["class"],
|
||||
blockquote: ["class"],
|
||||
},
|
||||
stripIgnoreTag: false,
|
||||
escapeHtml: (unsafeHtml) =>
|
||||
stringifyEntitiesLight(unsafeHtml, {
|
||||
escapeOnly: true,
|
||||
}),
|
||||
...extraConfig,
|
||||
}) as string;
|
||||
});
|
||||
|
||||
// Check text to only allow h-*, p-*, u-*, dt-*, e-*, mention, hashtag, ellipsis, invisible classes
|
||||
const allowedClasses = [
|
||||
|
|
|
|||
Loading…
Reference in a new issue