Compare commits

...

3 commits

Author SHA1 Message Date
Jesse Wierzbinski 1fba91f772
chore(config): 👽 Update JSON schema files
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 8s
Build Docker Images / check (push) Failing after 7s
Build Docker Images / tests (push) Failing after 8s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
2025-05-28 03:06:49 +02:00
Jesse Wierzbinski 710f965144
fix(federation): 🔒 Enforce content filters for remote content as well 2025-05-28 02:59:26 +02:00
Jesse Wierzbinski c737aeba8e
fix(api): 🐛 Enforce emoji shortcode filters 2025-05-28 02:45:53 +02:00
7 changed files with 133 additions and 190 deletions

View file

@ -113,8 +113,14 @@ export default apiRoute((app) => {
"json", "json",
z z
.object({ .object({
shortcode: CustomEmojiSchema.shape.shortcode.max( shortcode: CustomEmojiSchema.shape.shortcode
config.validation.emojis.max_shortcode_characters, .max(config.validation.emojis.max_shortcode_characters)
.refine(
(s) =>
!config.validation.filters.emoji_shortcode.some(
(filter) => filter.test(s),
),
"Shortcode contains blocked words",
), ),
element: z element: z
.string() .string()

View file

@ -45,8 +45,14 @@ export default apiRoute((app) =>
validator( validator(
"json", "json",
z.object({ z.object({
shortcode: CustomEmojiSchema.shape.shortcode.max( shortcode: CustomEmojiSchema.shape.shortcode
config.validation.emojis.max_shortcode_characters, .max(config.validation.emojis.max_shortcode_characters)
.refine(
(s) =>
!config.validation.filters.emoji_shortcode.some(
(filter) => filter.test(s),
),
"Shortcode contains blocked words",
), ),
element: z element: z
.string() .string()

View file

@ -1,6 +1,12 @@
import { zodToJsonSchema } from "zod-to-json-schema"; import { zodToJsonSchema } from "zod-to-json-schema";
import { ConfigSchema } from "./schema.ts";
const jsonSchema = zodToJsonSchema(ConfigSchema, {}); await import("~/config.ts");
console.write(`${JSON.stringify(jsonSchema, null, 4)}\n`); // This is an awkward way to avoid import cycles for some reason
await (async () => {
const { ConfigSchema } = await import("./schema.ts");
const jsonSchema = zodToJsonSchema(ConfigSchema, {});
console.write(`${JSON.stringify(jsonSchema, null, 4)}\n`);
})();

View file

@ -500,9 +500,14 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
); );
const emojis = await Promise.all( const emojis = await Promise.all(
extensions?.["pub.versia:custom_emojis"]?.emojis.map((emoji) => extensions?.["pub.versia:custom_emojis"]?.emojis
Emoji.fetchFromRemote(emoji, instance), .filter(
) ?? [], (e) =>
!config.validation.filters.emoji_shortcode.some(
(filter) => filter.test(e.name),
),
)
.map((emoji) => Emoji.fetchFromRemote(emoji, instance)) ?? [],
); );
const mentions = ( const mentions = (

View file

@ -181,9 +181,7 @@ export class InboxProcessor {
try { try {
await new EntitySorter(this.body) await new EntitySorter(this.body)
.on(VersiaEntities.Note, async (n) => { .on(VersiaEntities.Note, (n) => InboxProcessor.processNote(n))
await Note.fromVersia(n);
})
.on(VersiaEntities.Follow, (f) => .on(VersiaEntities.Follow, (f) =>
InboxProcessor.processFollowRequest(f), InboxProcessor.processFollowRequest(f),
) )
@ -199,9 +197,7 @@ export class InboxProcessor {
.on(VersiaEntities.Delete, (d) => .on(VersiaEntities.Delete, (d) =>
InboxProcessor.processDelete(d), InboxProcessor.processDelete(d),
) )
.on(VersiaEntities.User, async (u) => { .on(VersiaEntities.User, (u) => InboxProcessor.processUser(u))
await User.fromVersia(u);
})
.on(VersiaEntities.Share, async (s) => .on(VersiaEntities.Share, async (s) =>
InboxProcessor.processShare(s), InboxProcessor.processShare(s),
) )
@ -213,6 +209,66 @@ export class InboxProcessor {
} }
} }
/**
* Handles Note entity processing
*
* @param {VersiaNote} note - The Note entity to process.
* @returns {Promise<void>}
*/
private static async processNote(note: VersiaEntities.Note): Promise<void> {
// If note has a blocked word
if (
Object.values(note.content?.data ?? {})
.flatMap((c) => c.content)
.some((content) =>
config.validation.filters.note_content.some((filter) =>
filter.test(content),
),
)
) {
// Drop silently
return;
}
await Note.fromVersia(note);
}
/**
* Handles User entity processing.
*
* @param {VersiaUser} user - The User entity to process.
* @returns {Promise<void>}
*/
private static async processUser(user: VersiaEntities.User): Promise<void> {
if (
config.validation.filters.username.some((filter) =>
filter.test(user.data.username),
) ||
(user.data.display_name &&
config.validation.filters.displayname.some((filter) =>
filter.test(user.data.display_name ?? ""),
))
) {
// Drop silently
return;
}
if (
Object.values(user.bio?.data ?? {})
.flatMap((c) => c.content)
.some((content) =>
config.validation.filters.bio.some((filter) =>
filter.test(content),
),
)
) {
// Drop silently
return;
}
await User.fromVersia(user);
}
/** /**
* Handles Follow entity processing. * Handles Follow entity processing.
* *

View file

@ -57,27 +57,14 @@
"default": "versia" "default": "versia"
} }
}, },
"required": [ "required": ["host", "username"],
"host",
"port",
"username",
"password",
"database"
],
"additionalProperties": false "additionalProperties": false
}, },
"description": "Additional read-only replicas", "description": "Additional read-only replicas",
"default": [] "default": []
} }
}, },
"required": [ "required": ["username"],
"host",
"port",
"username",
"password",
"database",
"replicas"
],
"additionalProperties": false, "additionalProperties": false,
"description": "PostgreSQL database configuration" "description": "PostgreSQL database configuration"
}, },
@ -106,7 +93,6 @@
"default": 0 "default": 0
} }
}, },
"required": ["host", "port", "password", "database"],
"additionalProperties": false, "additionalProperties": false,
"description": "A Redis database used for managing queues." "description": "A Redis database used for managing queues."
}, },
@ -132,12 +118,11 @@
"default": 1 "default": 1
} }
}, },
"required": ["host", "port", "password", "database"],
"additionalProperties": false, "additionalProperties": false,
"description": "A Redis database used for caching SQL queries. Optional." "description": "A Redis database used for caching SQL queries. Optional."
} }
}, },
"required": ["queue", "cache"], "required": ["queue"],
"additionalProperties": false, "additionalProperties": false,
"description": "Redis configuration. Used for queues and caching." "description": "Redis configuration. Used for queues and caching."
}, },
@ -165,12 +150,11 @@
"$ref": "#/properties/postgres/properties/password" "$ref": "#/properties/postgres/properties/password"
} }
}, },
"required": ["host", "port", "password"], "required": ["password"],
"additionalProperties": false, "additionalProperties": false,
"description": "Sonic database configuration" "description": "Sonic database configuration"
} }
}, },
"required": ["enabled", "sonic"],
"additionalProperties": false, "additionalProperties": false,
"description": "Search and indexing configuration" "description": "Search and indexing configuration"
}, },
@ -191,7 +175,6 @@
"description": "Message to show to users when registration is disabled" "description": "Message to show to users when registration is disabled"
} }
}, },
"required": ["allow", "require_approval", "message"],
"additionalProperties": false "additionalProperties": false
}, },
"http": { "http": {
@ -250,20 +233,12 @@
"description": "This value must be a file path" "description": "This value must be a file path"
} }
}, },
"required": ["key", "cert", "passphrase", "ca"], "required": ["key", "cert"],
"additionalProperties": false, "additionalProperties": false,
"description": "TLS configuration. You should probably be using a reverse proxy instead of this" "description": "TLS configuration. You should probably be using a reverse proxy instead of this"
} }
}, },
"required": [ "required": ["base_url"],
"base_url",
"bind",
"bind_port",
"banned_ips",
"banned_user_agents",
"proxy_address",
"tls"
],
"additionalProperties": false "additionalProperties": false
}, },
"frontend": { "frontend": {
@ -275,7 +250,7 @@
}, },
"path": { "path": {
"type": "string", "type": "string",
"default": "/home/jessew/Dev/versia-server" "default": "frontend"
}, },
"routes": { "routes": {
"type": "object", "type": "object",
@ -302,13 +277,6 @@
"default": "/oauth/reset" "default": "/oauth/reset"
} }
}, },
"required": [
"home",
"login",
"consent",
"register",
"password_reset"
],
"additionalProperties": false "additionalProperties": false
}, },
"settings": { "settings": {
@ -317,7 +285,7 @@
"default": {} "default": {}
} }
}, },
"required": ["enabled", "path", "routes", "settings"], "required": ["routes"],
"additionalProperties": false "additionalProperties": false
}, },
"email": { "email": {
@ -351,17 +319,10 @@
"default": true "default": true
} }
}, },
"required": [ "required": ["server", "username"],
"server",
"port",
"username",
"password",
"tls"
],
"additionalProperties": false "additionalProperties": false
} }
}, },
"required": ["send_emails", "smtp"],
"additionalProperties": false "additionalProperties": false
}, },
"media": { "media": {
@ -393,15 +354,10 @@
"default": false "default": false
} }
}, },
"required": [
"convert_images",
"convert_to",
"convert_vectors"
],
"additionalProperties": false "additionalProperties": false
} }
}, },
"required": ["backend", "uploads_path", "conversion"], "required": ["conversion"],
"additionalProperties": false "additionalProperties": false
}, },
"s3": { "s3": {
@ -425,14 +381,19 @@
"public_url": { "public_url": {
"$ref": "#/properties/http/properties/base_url", "$ref": "#/properties/http/properties/base_url",
"description": "Public URL that uploaded media will be accessible at" "description": "Public URL that uploaded media will be accessible at"
},
"path": {
"type": "string"
},
"path_style": {
"type": "boolean",
"default": true
} }
}, },
"required": [ "required": [
"endpoint", "endpoint",
"access_key", "access_key",
"secret_access_key", "secret_access_key",
"region",
"bucket_name",
"public_url" "public_url"
], ],
"additionalProperties": false "additionalProperties": false
@ -518,18 +479,6 @@
"default": 20 "default": 20
} }
}, },
"required": [
"max_displayname_characters",
"max_username_characters",
"max_bio_characters",
"max_avatar_bytes",
"max_header_bytes",
"disallowed_usernames",
"max_field_count",
"max_field_name_characters",
"max_field_value_characters",
"max_pinned_notes"
],
"additionalProperties": false "additionalProperties": false
}, },
"notes": { "notes": {
@ -570,11 +519,6 @@
"default": 16 "default": 16
} }
}, },
"required": [
"max_characters",
"allowed_url_schemes",
"max_attachments"
],
"additionalProperties": false "additionalProperties": false
}, },
"media": { "media": {
@ -1838,11 +1782,6 @@
] ]
} }
}, },
"required": [
"max_bytes",
"max_description_characters",
"allowed_mime_types"
],
"additionalProperties": false "additionalProperties": false
}, },
"emojis": { "emojis": {
@ -1864,11 +1803,6 @@
"default": 1000 "default": 1000
} }
}, },
"required": [
"max_bytes",
"max_shortcode_characters",
"max_description_characters"
],
"additionalProperties": false "additionalProperties": false
}, },
"polls": { "polls": {
@ -1895,12 +1829,6 @@
"default": 8640000 "default": 8640000
} }
}, },
"required": [
"max_options",
"max_option_characters",
"min_duration_seconds",
"max_duration_seconds"
],
"additionalProperties": false "additionalProperties": false
}, },
"emails": { "emails": {
@ -1919,7 +1847,6 @@
"default": [] "default": []
} }
}, },
"required": ["disallow_tempmail", "disallowed_domains"],
"additionalProperties": false "additionalProperties": false
}, },
"challenges": { "challenges": {
@ -1940,7 +1867,7 @@
"description": "You can use PATH:/path/to/file to load this value from a file" "description": "You can use PATH:/path/to/file to load this value from a file"
} }
}, },
"required": ["difficulty", "expiration", "key"], "required": ["key"],
"additionalProperties": false, "additionalProperties": false,
"description": "CAPTCHA challenge configuration. Challenges are disabled if not provided." "description": "CAPTCHA challenge configuration. Challenges are disabled if not provided."
}, },
@ -1983,13 +1910,6 @@
"default": [] "default": []
} }
}, },
"required": [
"note_content",
"emoji_shortcode",
"username",
"displayname",
"bio"
],
"additionalProperties": false, "additionalProperties": false,
"description": "Block content that matches these regular expressions" "description": "Block content that matches these regular expressions"
} }
@ -2001,7 +1921,6 @@
"emojis", "emojis",
"polls", "polls",
"emails", "emails",
"challenges",
"filters" "filters"
], ],
"additionalProperties": false "additionalProperties": false
@ -2016,13 +1935,14 @@
"type": "object", "type": "object",
"properties": { "properties": {
"public": { "public": {
"$ref": "#/properties/postgres/properties/password" "$ref": "#/properties/postgres/properties/password",
"description": "You can use PATH:/path/to/file to load this value from a file"
}, },
"private": { "private": {
"$ref": "#/properties/postgres/properties/password" "$ref": "#/properties/postgres/properties/password",
"description": "You can use PATH:/path/to/file to load this value from a file"
} }
}, },
"required": ["public", "private"],
"additionalProperties": false "additionalProperties": false
}, },
"subject": { "subject": {
@ -2030,12 +1950,11 @@
"description": "Subject field embedded in the push notification. Example: 'mailto:contact@example.com'" "description": "Subject field embedded in the push notification. Example: 'mailto:contact@example.com'"
} }
}, },
"required": ["vapid_keys", "subject"], "required": ["vapid_keys"],
"additionalProperties": false, "additionalProperties": false,
"description": "Web Push Notifications configuration. Leave out to disable." "description": "Web Push Notifications configuration. Leave out to disable."
} }
}, },
"required": ["push"],
"additionalProperties": false "additionalProperties": false
}, },
"defaults": { "defaults": {
@ -2062,13 +1981,6 @@
"description": "A style name from https://www.dicebear.com/styles" "description": "A style name from https://www.dicebear.com/styles"
} }
}, },
"required": [
"visibility",
"language",
"avatar",
"header",
"placeholder_style"
],
"additionalProperties": false "additionalProperties": false
}, },
"federation": { "federation": {
@ -2155,17 +2067,6 @@
"default": [] "default": []
} }
}, },
"required": [
"reports",
"deletes",
"updates",
"media",
"follows",
"likes",
"reactions",
"banners",
"avatars"
],
"additionalProperties": false "additionalProperties": false
}, },
"bridge": { "bridge": {
@ -2196,11 +2097,11 @@
"$ref": "#/properties/http/properties/proxy_address" "$ref": "#/properties/http/properties/proxy_address"
} }
}, },
"required": ["software", "allowed_ips", "token", "url"], "required": ["software", "token", "url"],
"additionalProperties": false "additionalProperties": false
} }
}, },
"required": ["blocked", "followers_only", "discard", "bridge"], "required": ["discard"],
"additionalProperties": false "additionalProperties": false
}, },
"queues": { "queues": {
@ -2219,10 +2120,6 @@
"default": 31536000 "default": 31536000
} }
}, },
"required": [
"remove_after_complete_seconds",
"remove_after_failure_seconds"
],
"additionalProperties": false "additionalProperties": false
}, },
"propertyNames": { "propertyNames": {
@ -2264,7 +2161,6 @@
"$ref": "#/properties/http/properties/proxy_address" "$ref": "#/properties/http/properties/proxy_address"
} }
}, },
"required": ["logo", "banner"],
"additionalProperties": false "additionalProperties": false
}, },
"languages": { "languages": {
@ -2489,7 +2385,7 @@
"description": "Longer version of the rule with additional information" "description": "Longer version of the rule with additional information"
} }
}, },
"required": ["text", "hint"], "required": ["text"],
"additionalProperties": false "additionalProperties": false
}, },
"default": [] "default": []
@ -2498,28 +2394,18 @@
"type": "object", "type": "object",
"properties": { "properties": {
"public": { "public": {
"$ref": "#/properties/postgres/properties/password" "$ref": "#/properties/postgres/properties/password",
"description": "You can use PATH:/path/to/file to load this value from a file"
}, },
"private": { "private": {
"$ref": "#/properties/postgres/properties/password" "$ref": "#/properties/postgres/properties/password",
"description": "You can use PATH:/path/to/file to load this value from a file"
} }
}, },
"required": ["public", "private"],
"additionalProperties": false "additionalProperties": false
} }
}, },
"required": [ "required": ["branding", "languages", "contact"],
"name",
"description",
"extended_description_path",
"tos_path",
"privacy_policy_path",
"branding",
"languages",
"contact",
"rules",
"keys"
],
"additionalProperties": false "additionalProperties": false
}, },
"permissions": { "permissions": {
@ -2798,7 +2684,6 @@
] ]
} }
}, },
"required": ["anonymous", "default", "admin"],
"additionalProperties": false "additionalProperties": false
}, },
"logging": { "logging": {
@ -2830,7 +2715,6 @@
"type": "string" "type": "string"
} }
}, },
"required": ["level", "log_file_path"],
"additionalProperties": false "additionalProperties": false
} }
] ]
@ -2886,15 +2770,7 @@
"type": "string" "type": "string"
} }
}, },
"required": [ "required": ["dsn"],
"dsn",
"debug",
"sample_rate",
"traces_sample_rate",
"trace_propagation_targets",
"max_breadcrumbs",
"environment"
],
"additionalProperties": false "additionalProperties": false
}, },
"log_file_path": { "log_file_path": {
@ -2902,7 +2778,7 @@
"default": "logs/versia.log" "default": "logs/versia.log"
} }
}, },
"required": ["types", "log_level", "sentry", "log_file_path"], "required": ["types"],
"additionalProperties": false "additionalProperties": false
}, },
"debug": { "debug": {
@ -2913,7 +2789,6 @@
"default": false "default": false
} }
}, },
"required": ["federation"],
"additionalProperties": false "additionalProperties": false
}, },
"plugins": { "plugins": {
@ -2941,7 +2816,6 @@
"default": [] "default": []
} }
}, },
"required": ["enabled", "disabled"],
"additionalProperties": false "additionalProperties": false
}, },
"config": { "config": {
@ -2949,7 +2823,7 @@
"additionalProperties": {} "additionalProperties": {}
} }
}, },
"required": ["autoload", "overrides", "config"], "required": ["overrides"],
"additionalProperties": false "additionalProperties": false
} }
}, },
@ -2962,7 +2836,6 @@
"frontend", "frontend",
"email", "email",
"media", "media",
"s3",
"validation", "validation",
"notifications", "notifications",
"defaults", "defaults",
@ -2971,7 +2844,6 @@
"instance", "instance",
"permissions", "permissions",
"logging", "logging",
"debug",
"plugins" "plugins"
], ],
"additionalProperties": false, "additionalProperties": false,

View file

@ -37,7 +37,7 @@
"format": "uri" "format": "uri"
} }
}, },
"required": ["name", "email", "url"], "required": ["name"],
"additionalProperties": false "additionalProperties": false
} }
}, },
@ -75,18 +75,10 @@
"format": "uri" "format": "uri"
} }
}, },
"required": ["type", "url"],
"additionalProperties": false "additionalProperties": false
} }
}, },
"required": [ "required": ["name", "version", "description"],
"$schema",
"name",
"version",
"description",
"authors",
"repository"
],
"additionalProperties": false, "additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#" "$schema": "http://json-schema.org/draft-07/schema#"
} }