feat: security hardening, DB indexes, token revocation, and input validation

- SSRF protection: DNS resolution + private IP blocking on metadata fetcher
- Upload security: CSP/X-Frame-Options headers, SVG forced download, nosniff
- Auth hardening: JWT secret min length, password min 8 chars, token revocation via password_changed_at
- Attachment ownership verification before linking to messages
- Message length limit (4000 chars) enforced on client and server
- Asset URL validation on avatar/banner updates
- Federation instance validation (domain regex, origin scheme, length limits)
- DB indexes on all FK columns for query performance
- Migrations: nullable moderator columns, dm_messages reply_to FK constraint
- File cleanup on avatar/banner replacement and space deletion
- Fastify trustProxy, AbortController on fetches, typing map size cap
This commit is contained in:
Jannis Braun
2026-03-15 00:06:15 +01:00
parent ed4dcdcf69
commit 7c544c1ff4
37 changed files with 892 additions and 178 deletions
+67 -1
View File
@@ -24,8 +24,18 @@ function createTables(db: Database.Database): void {
avatar TEXT,
status TEXT DEFAULT 'offline',
custom_status TEXT,
is_admin INTEGER DEFAULT 0,
home_instance TEXT,
home_user_id TEXT,
replicated_instances TEXT DEFAULT '[]',
banner TEXT,
accent_color TEXT,
avatar_color TEXT,
bio TEXT,
is_deleted INTEGER DEFAULT 0,
discoverable INTEGER DEFAULT 1,
profile_updated_at INTEGER,
password_changed_at INTEGER,
created_at INTEGER NOT NULL
);
@@ -33,8 +43,12 @@ function createTables(db: Database.Database): void {
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
icon TEXT,
banner TEXT,
avatar_color TEXT,
owner_id TEXT NOT NULL REFERENCES users(id),
invite_code TEXT UNIQUE,
visibility TEXT DEFAULT 'private',
description TEXT,
created_at INTEGER NOT NULL
);
@@ -46,6 +60,14 @@ function createTables(db: Database.Database): void {
PRIMARY KEY (space_id, user_id)
);
CREATE TABLE IF NOT EXISTS channel_categories (
id TEXT PRIMARY KEY,
space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE,
name TEXT NOT NULL,
position INTEGER DEFAULT 0,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS channels (
id TEXT PRIMARY KEY,
space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE,
@@ -53,6 +75,7 @@ function createTables(db: Database.Database): void {
type TEXT NOT NULL,
topic TEXT,
position INTEGER DEFAULT 0,
category_id TEXT,
created_at INTEGER NOT NULL
);
@@ -69,10 +92,13 @@ function createTables(db: Database.Database): void {
CREATE TABLE IF NOT EXISTS attachments (
id TEXT PRIMARY KEY,
message_id TEXT REFERENCES messages(id) ON DELETE CASCADE,
dm_message_id TEXT,
uploader_id TEXT,
filename TEXT NOT NULL,
original_name TEXT NOT NULL,
mimetype TEXT NOT NULL,
size INTEGER NOT NULL,
thumbnail_filename TEXT,
created_at INTEGER NOT NULL
);
@@ -85,6 +111,7 @@ function createTables(db: Database.Database): void {
CREATE TABLE IF NOT EXISTS dm_members (
dm_channel_id TEXT NOT NULL REFERENCES dm_channels(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
closed INTEGER DEFAULT 0,
PRIMARY KEY (dm_channel_id, user_id)
);
@@ -92,7 +119,9 @@ function createTables(db: Database.Database): void {
id TEXT PRIMARY KEY,
dm_channel_id TEXT NOT NULL REFERENCES dm_channels(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id),
reply_to_id TEXT REFERENCES dm_messages(id) ON DELETE SET NULL,
content TEXT,
edited_at INTEGER,
created_at INTEGER NOT NULL
);
@@ -122,7 +151,7 @@ function createTables(db: Database.Database): void {
CREATE TABLE IF NOT EXISTS dm_reactions (
id TEXT PRIMARY KEY,
dm_message_id TEXT NOT NULL,
dm_message_id TEXT NOT NULL REFERENCES dm_messages(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
emoji TEXT NOT NULL,
created_at INTEGER NOT NULL,
@@ -179,10 +208,17 @@ function createTables(db: Database.Database): void {
PRIMARY KEY (folder_id, space_id)
);
CREATE TABLE IF NOT EXISTS user_space_layout (
user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
layout TEXT NOT NULL DEFAULT '[]',
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS instance_settings (
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
instance_name TEXT DEFAULT 'Backspace',
worker_id INTEGER,
discovery_enabled INTEGER NOT NULL DEFAULT 1,
max_bitrate_kbps INTEGER NOT NULL DEFAULT 20000,
min_bitrate_kbps INTEGER NOT NULL DEFAULT 500,
bitrate_step_kbps INTEGER NOT NULL DEFAULT 500,
@@ -190,8 +226,38 @@ function createTables(db: Database.Database): void {
allowed_framerates TEXT NOT NULL DEFAULT '30,45,60',
max_resolution INTEGER NOT NULL DEFAULT 1080,
max_framerate INTEGER NOT NULL DEFAULT 60,
registration_open INTEGER,
updated_at INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS bans (
space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
reason TEXT,
banned_by TEXT REFERENCES users(id),
created_at INTEGER NOT NULL,
PRIMARY KEY (space_id, user_id)
);
CREATE TABLE IF NOT EXISTS join_requests (
id TEXT PRIMARY KEY,
space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
message TEXT,
status TEXT NOT NULL DEFAULT 'pending',
decided_by TEXT REFERENCES users(id),
created_at INTEGER NOT NULL,
decided_at INTEGER
);
CREATE TABLE IF NOT EXISTS voice_restrictions (
space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
restriction_type TEXT NOT NULL,
moderator_id TEXT REFERENCES users(id),
created_at INTEGER NOT NULL,
PRIMARY KEY (space_id, user_id, restriction_type)
);
`);
}