feat(api): Allow more HTML tags in Markdown

This commit is contained in:
Jesse Wierzbinski 2024-05-11 15:27:19 -10:00
parent 4ce5dfeae3
commit b979daa39a
No known key found for this signature in database
3 changed files with 100 additions and 9 deletions

View file

@ -1,7 +1,7 @@
import { applyConfig, auth, handleZodError, qs } from "@api"; import { applyConfig, auth, handleZodError, qs } from "@api";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { sanitizeHtml, sanitizedHtmlStrip } from "@sanitization"; import { sanitizedHtmlStrip } from "@sanitization";
import { config } from "config-manager"; import { config } from "config-manager";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import type { Hono } from "hono"; import type { Hono } from "hono";
@ -224,17 +224,25 @@ export default (app: Hono) =>
self.source.fields = []; self.source.fields = [];
for (const field of fields_attributes) { for (const field of fields_attributes) {
// Can be Markdown or plaintext, also has emojis // Can be Markdown or plaintext, also has emojis
const parsedName = await contentToHtml({ const parsedName = await contentToHtml(
{
"text/markdown": { "text/markdown": {
content: field.name, content: field.name,
}, },
}); },
undefined,
true,
);
const parsedValue = await contentToHtml({ const parsedValue = await contentToHtml(
{
"text/markdown": { "text/markdown": {
content: field.value, content: field.value,
}, },
}); },
undefined,
true,
);
// Parse emojis // Parse emojis
const nameEmojis = await parseEmojis(parsedName); const nameEmojis = await parseEmojis(parsedName);

View file

@ -394,5 +394,37 @@ describe(meta.route, () => {
"uwu <script>alert('Hello, world!');</script>", "uwu <script>alert('Hello, world!');</script>",
); );
}); });
test("should rewrite all image and video src to go through proxy", async () => {
const response = await sendTestRequest(
new Request(new URL(meta.route, config.http.base_url), {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].accessToken}`,
},
body: new URLSearchParams({
status: "<img src='https://example.com/image.jpg'> <video src='https://example.com/video.mp4'> Test!",
federate: "false",
}),
}),
);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe(
"application/json",
);
const object = (await response.json()) as APIStatus;
// Proxy url is base_url/media/proxy/<base64url encoded url>
expect(object.content).toBe(
`<p><img src="${config.http.base_url}/media/proxy/${Buffer.from(
"https://example.com/image.jpg",
).toString("base64url")}"> <video src="${
config.http.base_url
}/media/proxy/${Buffer.from(
"https://example.com/video.mp4",
).toString("base64url")}"> Test!</p>`,
);
});
}); });
}); });

View file

@ -7,6 +7,29 @@ export const sanitizedHtmlStrip = (html: string) => {
}); });
}; };
export const sanitizeHtmlInline = async (
html: string,
extraConfig?: IFilterXSSOptions,
) => {
return sanitizeHtml(html, {
whiteList: {
a: ["href", "title", "target", "rel", "class"],
p: ["class"],
b: ["class"],
i: ["class"],
em: ["class"],
strong: ["class"],
del: ["class"],
u: ["class"],
font: ["color", "size", "face", "class"],
strike: ["class"],
mark: ["class"],
small: ["class"],
},
...extraConfig,
});
};
export const sanitizeHtml = async ( export const sanitizeHtml = async (
html: string, html: string,
extraConfig?: IFilterXSSOptions, extraConfig?: IFilterXSSOptions,
@ -28,6 +51,34 @@ export const sanitizeHtml = async (
ol: ["class"], ol: ["class"],
li: ["class"], li: ["class"],
blockquote: ["class"], blockquote: ["class"],
h1: ["class"],
h2: ["class"],
h3: ["class"],
h4: ["class"],
h5: ["class"],
h6: ["class"],
img: ["src", "alt", "title", "class"],
font: ["color", "size", "face", "class"],
table: ["class"],
tr: ["class"],
td: ["class"],
th: ["class"],
tbody: ["class"],
thead: ["class"],
tfoot: ["class"],
hr: ["class"],
strike: ["class"],
figcaption: ["class"],
figure: ["class"],
mark: ["class"],
summary: ["class"],
details: ["class"],
caption: ["class"],
small: ["class"],
video: ["class", "src", "controls"],
audio: ["class", "src", "controls"],
source: ["src", "type"],
track: ["src", "label", "kind"],
}, },
stripIgnoreTag: false, stripIgnoreTag: false,
escapeHtml: (unsafeHtml) => escapeHtml: (unsafeHtml) =>