mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
Add sanitization to HTML
This commit is contained in:
parent
3c289dd3de
commit
f677737fdd
|
|
@ -263,8 +263,8 @@ export class User extends BaseEntity {
|
||||||
|
|
||||||
await user.generateKeys();
|
await user.generateKeys();
|
||||||
await user.updateActor();
|
await user.updateActor();
|
||||||
|
|
||||||
await user.save();
|
await user.save();
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -403,6 +403,7 @@ export class User extends BaseEntity {
|
||||||
outbox: `${config.http.base_url}/users/${this.username}/outbox`,
|
outbox: `${config.http.base_url}/users/${this.username}/outbox`,
|
||||||
followers: `${config.http.base_url}/users/${this.username}/followers`,
|
followers: `${config.http.base_url}/users/${this.username}/followers`,
|
||||||
following: `${config.http.base_url}/users/${this.username}/following`,
|
following: `${config.http.base_url}/users/${this.username}/following`,
|
||||||
|
published: new Date(this.created_at).toISOString(),
|
||||||
manuallyApprovesFollowers: false,
|
manuallyApprovesFollowers: false,
|
||||||
summary: this.note,
|
summary: this.note,
|
||||||
icon: {
|
icon: {
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,9 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ip-matching": "^2.1.2",
|
"ip-matching": "^2.1.2",
|
||||||
|
"isomorphic-dompurify": "^1.9.0",
|
||||||
"jsonld": "^8.3.1",
|
"jsonld": "^8.3.1",
|
||||||
|
"marked": "^9.1.2",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"typeorm": "^0.3.17"
|
"typeorm": "^0.3.17"
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { parseRequest } from "@request";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { User } from "~database/entities/User";
|
import { User } from "~database/entities/User";
|
||||||
import { applyConfig } from "@api";
|
import { applyConfig } from "@api";
|
||||||
|
import { sanitize } from "isomorphic-dompurify";
|
||||||
|
import { sanitizeHtml } from "@sanitization";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["PATCH"],
|
allowedMethods: ["PATCH"],
|
||||||
|
|
@ -50,11 +52,18 @@ export default async (req: Request): Promise<Response> => {
|
||||||
"source[language]": string;
|
"source[language]": string;
|
||||||
}>(req);
|
}>(req);
|
||||||
|
|
||||||
|
const sanitizedNote = await sanitizeHtml(note ?? "");
|
||||||
|
|
||||||
|
const sanitizedDisplayName = sanitize(display_name, {
|
||||||
|
ALLOWED_TAGS: [],
|
||||||
|
ALLOWED_ATTR: [],
|
||||||
|
});
|
||||||
|
|
||||||
if (display_name) {
|
if (display_name) {
|
||||||
// Check if within allowed display name lengths
|
// Check if within allowed display name lengths
|
||||||
if (
|
if (
|
||||||
display_name.length < 3 ||
|
sanitizedDisplayName.length < 3 ||
|
||||||
display_name.length > config.validation.max_displayname_size
|
sanitizedDisplayName.length > config.validation.max_displayname_size
|
||||||
) {
|
) {
|
||||||
return errorResponse(
|
return errorResponse(
|
||||||
`Display name must be between 3 and ${config.validation.max_displayname_size} characters`,
|
`Display name must be between 3 and ${config.validation.max_displayname_size} characters`,
|
||||||
|
|
@ -65,19 +74,19 @@ export default async (req: Request): Promise<Response> => {
|
||||||
// Check if display name doesnt match filters
|
// Check if display name doesnt match filters
|
||||||
if (
|
if (
|
||||||
config.filters.displayname_filters.some(filter =>
|
config.filters.displayname_filters.some(filter =>
|
||||||
display_name.match(filter)
|
sanitizedDisplayName.match(filter)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return errorResponse("Display name contains blocked words", 422);
|
return errorResponse("Display name contains blocked words", 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
user.actor.data.name = display_name;
|
user.actor.data.name = sanitizedDisplayName;
|
||||||
user.display_name = display_name;
|
user.display_name = sanitizedDisplayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note) {
|
if (note) {
|
||||||
// Check if within allowed note length
|
// Check if within allowed note length
|
||||||
if (note.length > config.validation.max_note_size) {
|
if (sanitizedNote.length > config.validation.max_note_size) {
|
||||||
return errorResponse(
|
return errorResponse(
|
||||||
`Note must be less than ${config.validation.max_note_size} characters`,
|
`Note must be less than ${config.validation.max_note_size} characters`,
|
||||||
422
|
422
|
||||||
|
|
@ -85,12 +94,16 @@ export default async (req: Request): Promise<Response> => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if bio doesnt match filters
|
// Check if bio doesnt match filters
|
||||||
if (config.filters.bio_filters.some(filter => note.match(filter))) {
|
if (
|
||||||
|
config.filters.bio_filters.some(filter =>
|
||||||
|
sanitizedNote.match(filter)
|
||||||
|
)
|
||||||
|
) {
|
||||||
return errorResponse("Bio contains blocked words", 422);
|
return errorResponse("Bio contains blocked words", 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
user.actor.data.summary = note;
|
user.actor.data.summary = sanitizedNote;
|
||||||
user.note = note;
|
user.note = sanitizedNote;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (source_privacy) {
|
if (source_privacy) {
|
||||||
|
|
@ -177,6 +190,7 @@ export default async (req: Request): Promise<Response> => {
|
||||||
}
|
}
|
||||||
|
|
||||||
await user.save();
|
await user.save();
|
||||||
|
await user.updateActor();
|
||||||
|
|
||||||
return jsonResponse(await user.toAPI());
|
return jsonResponse(await user.toAPI());
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@ import { applyConfig } from "@api";
|
||||||
import { getConfig } from "@config";
|
import { getConfig } from "@config";
|
||||||
import { parseRequest } from "@request";
|
import { parseRequest } from "@request";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
import { sanitizeHtml } from "@sanitization";
|
||||||
import { APActor } from "activitypub-types";
|
import { APActor } from "activitypub-types";
|
||||||
|
import { sanitize } from "isomorphic-dompurify";
|
||||||
import { Application } from "~database/entities/Application";
|
import { Application } from "~database/entities/Application";
|
||||||
import { RawObject } from "~database/entities/RawObject";
|
import { RawObject } from "~database/entities/RawObject";
|
||||||
import { Status } from "~database/entities/Status";
|
import { Status } from "~database/entities/Status";
|
||||||
|
|
@ -72,7 +74,9 @@ export default async (req: Request): Promise<Response> => {
|
||||||
return errorResponse("Status is required", 422);
|
return errorResponse("Status is required", 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.length > config.validation.max_note_size) {
|
const sanitizedStatus = await sanitizeHtml(status);
|
||||||
|
|
||||||
|
if (sanitizedStatus.length > config.validation.max_note_size) {
|
||||||
return errorResponse(
|
return errorResponse(
|
||||||
`Status must be less than ${config.validation.max_note_size} characters`,
|
`Status must be less than ${config.validation.max_note_size} characters`,
|
||||||
400
|
400
|
||||||
|
|
@ -134,7 +138,7 @@ export default async (req: Request): Promise<Response> => {
|
||||||
const newStatus = await Status.createNew({
|
const newStatus = await Status.createNew({
|
||||||
account: user,
|
account: user,
|
||||||
application,
|
application,
|
||||||
content: status,
|
content: sanitizedStatus,
|
||||||
visibility:
|
visibility:
|
||||||
visibility ||
|
visibility ||
|
||||||
(config.defaults.visibility as
|
(config.defaults.visibility as
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ export const configDefaults: ConfigType = {
|
||||||
http: {
|
http: {
|
||||||
bind: "http://0.0.0.0",
|
bind: "http://0.0.0.0",
|
||||||
bind_port: "8000",
|
bind_port: "8000",
|
||||||
base_url: "http://fediproject.localhost:8000",
|
base_url: "http://lysand.localhost:8000",
|
||||||
banned_ips: [],
|
banned_ips: [],
|
||||||
},
|
},
|
||||||
database: {
|
database: {
|
||||||
|
|
|
||||||
76
utils/sanitization.ts
Normal file
76
utils/sanitization.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { getConfig } from "@config";
|
||||||
|
import { sanitize } from "isomorphic-dompurify";
|
||||||
|
|
||||||
|
export const sanitizeHtml = async (html: string) => {
|
||||||
|
const config = getConfig();
|
||||||
|
|
||||||
|
const sanitizedHtml = 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check text to only allow h-*, p-*, u-*, dt-*, e-*, mention, hashtag, ellipsis, invisible classes
|
||||||
|
const allowedClasses = [
|
||||||
|
"h-",
|
||||||
|
"p-",
|
||||||
|
"u-",
|
||||||
|
"dt-",
|
||||||
|
"e-",
|
||||||
|
"mention",
|
||||||
|
"hashtag",
|
||||||
|
"ellipsis",
|
||||||
|
"invisible",
|
||||||
|
];
|
||||||
|
|
||||||
|
return await new HTMLRewriter()
|
||||||
|
.on("*[class]", {
|
||||||
|
element(element) {
|
||||||
|
const classes = element.getAttribute("class")?.split(" ") ?? [];
|
||||||
|
|
||||||
|
classes.forEach(className => {
|
||||||
|
if (
|
||||||
|
!allowedClasses.some(allowedClass =>
|
||||||
|
className.startsWith(allowedClass)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
element.removeAttribute("class");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.transform(new Response(sanitizedHtml))
|
||||||
|
.text();
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue