mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
fix(api): 🔒 Correctly put all URIs in profiles through proxy
This commit is contained in:
parent
bd1f09837b
commit
569ba8bf2d
68
api/api/v1/accounts/update_credentials/index.test.ts
Normal file
68
api/api/v1/accounts/update_credentials/index.test.ts
Normal 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! <script>alert('Hello, world!');</script></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`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue