fix(api): 🔒 Correctly put all URIs in profiles through proxy

This commit is contained in:
Jesse Wierzbinski 2024-11-22 15:06:46 +01:00
parent bd1f09837b
commit 569ba8bf2d
No known key found for this signature in database
3 changed files with 79 additions and 12 deletions

View file

@ -0,0 +1,68 @@
import { afterAll, describe, expect, test } from "bun:test";
import type { Account as APIAccount } from "@versia/client/types";
import { config } from "~/packages/config-manager/index.ts";
import { fakeRequest, getTestUsers } from "~/tests/utils";
import { meta } from "./index.ts";
const { tokens, deleteUsers } = await getTestUsers(1);
afterAll(async () => {
await deleteUsers();
});
// /api/v1/accounts/update_credentials
describe(meta.route, () => {
describe("HTML injection testing", () => {
test("should not allow HTML injection", async () => {
const response = await fakeRequest(meta.route, {
method: "PATCH",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams({
note: "Hi! <script>alert('Hello, world!');</script>",
}),
});
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain(
"application/json",
);
const object = (await response.json()) as APIAccount;
expect(object.note).toBe(
"<p>Hi! &lt;script&gt;alert('Hello, world!');&lt;/script&gt;</p>\n",
);
});
test("should rewrite all image and video src to go through proxy", async () => {
const response = await fakeRequest(meta.route, {
method: "PATCH",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams({
note: "<img src='https://example.com/image.jpg'> <video src='https://example.com/video.mp4'> Test!",
}),
});
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain(
"application/json",
);
const object = (await response.json()) as APIAccount;
// Proxy url is base_url/media/proxy/<base64url encoded url>
expect(object.note).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>\n`,
);
});
});
});

View file

@ -1,6 +1,5 @@
import { idValidator } from "@/api"; import { idValidator } from "@/api";
import { localObjectUri } from "@/constants"; import { localObjectUri } from "@/constants";
import { proxyUrl } from "@/response";
import { sanitizedHtmlStrip } from "@/sanitization"; import { sanitizedHtmlStrip } from "@/sanitization";
import { sentry } from "@/sentry"; import { sentry } from "@/sentry";
import { getLogger } from "@logtape/logtape"; import { getLogger } from "@logtape/logtape";
@ -902,17 +901,7 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
(mention) => mention.instanceId === null, (mention) => mention.instanceId === null,
); );
// Rewrite all src tags to go through proxy let replacedContent = data.content;
let replacedContent = new HTMLRewriter()
.on("[src]", {
element(element): void {
element.setAttribute(
"src",
proxyUrl(element.getAttribute("src") ?? "") ?? "",
);
},
})
.transform(data.content);
for (const mention of mentionedLocalUsers) { for (const mention of mentionedLocalUsers) {
replacedContent = replacedContent.replace( replacedContent = replacedContent.replace(

View file

@ -1,5 +1,6 @@
import { stringifyEntitiesLight } from "stringify-entities"; import { stringifyEntitiesLight } from "stringify-entities";
import xss, { type IFilterXSSOptions } from "xss"; import xss, { type IFilterXSSOptions } from "xss";
import { proxyUrl } from "./response.ts";
export const sanitizedHtmlStrip = (html: string): Promise<string> => { export const sanitizedHtmlStrip = (html: string): Promise<string> => {
return sanitizeHtml(html, { return sanitizeHtml(html, {
@ -129,6 +130,15 @@ export const sanitizeHtml = async (
} }
}, },
}) })
// Rewrite all src tags to go through proxy
.on("[src]", {
element(element): void {
element.setAttribute(
"src",
proxyUrl(element.getAttribute("src") ?? "") ?? "",
);
},
})
.transform(new Response(sanitizedHtml)) .transform(new Response(sanitizedHtml))
.text(); .text();
}; };