diff --git a/nix/module.nix b/nix/module.nix index 30af9b0..f411233 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -9,6 +9,29 @@ with lib; let cfg = config.services.archtika; + baseHardenedSystemdOptions = { + CapabilityBoundingSet = ""; + LockPersonality = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectSystem = "strict"; + RemoveIPC = true; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = ["@system-service" "~@privileged" "~@resources"]; + + ReadWritePaths = ["/var/www/archtika-websites"]; + }; in { options.services.archtika = { @@ -105,9 +128,17 @@ in group = cfg.group; }; - users.groups.${cfg.group} = { }; + users.groups.${cfg.group} = { + members = [ + "nginx" + "postgres" + ]; + }; - systemd.tmpfiles.rules = [ "d /var/www/archtika-websites 0777 ${cfg.user} ${cfg.group} -" ]; + systemd.tmpfiles.rules = [ + "d /var/www 0755 root root -" + "d /var/www/archtika-websites 0770 ${cfg.user} ${cfg.group} -" + ]; systemd.services.archtika-api = { description = "archtika API service"; @@ -117,11 +148,13 @@ in "postgresql.service" ]; - serviceConfig = { + serviceConfig = baseHardenedSystemdOptions // { User = cfg.user; Group = cfg.group; Restart = "always"; WorkingDirectory = "${cfg.package}/rest-api"; + + RestrictAddressFamilies = ["AF_INET" "AF_INET6" "AF_UNIX"]; }; script = '' @@ -142,11 +175,13 @@ in wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; - serviceConfig = { + serviceConfig = baseHardenedSystemdOptions // { User = cfg.user; Group = cfg.group; Restart = "always"; WorkingDirectory = "${cfg.package}/web-app"; + + RestrictAddressFamilies = ["AF_INET" "AF_INET6"]; }; script = '' diff --git a/rest-api/db/migrations/20240719071602_main_tables.sql b/rest-api/db/migrations/20240719071602_main_tables.sql index ee06a6d..5d88225 100644 --- a/rest-api/db/migrations/20240719071602_main_tables.sql +++ b/rest-api/db/migrations/20240719071602_main_tables.sql @@ -32,10 +32,15 @@ ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; CREATE FUNCTION internal.generate_slug (TEXT) RETURNS TEXT AS $$ - SELECT - REGEXP_REPLACE(REGEXP_REPLACE(REGEXP_REPLACE(REGEXP_REPLACE(LOWER(TRIM(REGEXP_REPLACE(unaccent ($1), '\s+', '-', 'g'))), '[^\w-]', '', 'g'), '-+', '-', 'g'), '^-+', '', 'g'), '-+$', '', 'g') +BEGIN + IF $1 ~ '[/\\.]' THEN + RAISE invalid_parameter_value + USING message = 'Title cannot contain "/", "\" or "."'; + END IF; + RETURN REGEXP_REPLACE(REGEXP_REPLACE(REGEXP_REPLACE(REGEXP_REPLACE(LOWER(TRIM(REGEXP_REPLACE(unaccent ($1), '\s+', '-', 'g'))), '[^\w-]', '', 'g'), '-+', '-', 'g'), '^-+', '', 'g'), '-+$', '', 'g'); +END; $$ -LANGUAGE sql +LANGUAGE plpgsql IMMUTABLE; GRANT EXECUTE ON FUNCTION internal.generate_slug TO authenticated_user; diff --git a/rest-api/db/migrations/20240720074103_user_management_roles_jwt.sql b/rest-api/db/migrations/20240720074103_user_management_roles_jwt.sql index 28af12b..fb5a582 100644 --- a/rest-api/db/migrations/20240720074103_user_management_roles_jwt.sql +++ b/rest-api/db/migrations/20240720074103_user_management_roles_jwt.sql @@ -120,7 +120,7 @@ AS $$ DECLARE _role NAME; _user_id UUID; - _exp INT := EXTRACT(EPOCH FROM CLOCK_TIMESTAMP())::INT + 86400; + _exp INT := EXTRACT(EPOCH FROM CLOCK_TIMESTAMP())::INT + 43200; BEGIN SELECT internal.user_role (login.username, login.pass) INTO _role; diff --git a/rest-api/db/migrations/20241011092744_filesystem_triggers.sql b/rest-api/db/migrations/20241011092744_filesystem_triggers.sql index ca6d043..6c3ff91 100644 --- a/rest-api/db/migrations/20241011092744_filesystem_triggers.sql +++ b/rest-api/db/migrations/20241011092744_filesystem_triggers.sql @@ -46,12 +46,12 @@ LANGUAGE plpgsql SECURITY DEFINER; CREATE TRIGGER _cleanup_filesystem_website - BEFORE UPDATE OR DELETE ON internal.website + BEFORE UPDATE OF title OR DELETE ON internal.website FOR EACH ROW EXECUTE FUNCTION internal.cleanup_filesystem (); CREATE TRIGGER _cleanup_filesystem_article - BEFORE UPDATE OR DELETE ON internal.article + BEFORE UPDATE OF title OR DELETE ON internal.article FOR EACH ROW EXECUTE FUNCTION internal.cleanup_filesystem (); diff --git a/rest-api/db/migrations/20241206191942_username_blocklist.sql b/rest-api/db/migrations/20241206191942_username_blocklist.sql new file mode 100644 index 0000000..476c5b5 --- /dev/null +++ b/rest-api/db/migrations/20241206191942_username_blocklist.sql @@ -0,0 +1,8 @@ +-- migrate:up +ALTER TABLE internal.user + ADD CONSTRAINT username_not_blocked CHECK (LOWER(username) NOT IN ('admin', 'administrator', 'api', 'auth', 'blog', 'cdn', 'docs', 'help', 'login', 'logout', 'profile', 'register', 'settings', 'setup', 'signin', 'signup', 'support', 'test', 'www')); + +-- migrate:down +ALTER TABLE internal.user + DROP CONSTRAINT username_not_blocked; + diff --git a/web-app/src/lib/components/WebsiteEditor.svelte b/web-app/src/lib/components/WebsiteEditor.svelte index 14e1f21..cbecd43 100644 --- a/web-app/src/lib/components/WebsiteEditor.svelte +++ b/web-app/src/lib/components/WebsiteEditor.svelte @@ -26,6 +26,8 @@ }); const tabs = ["settings", "articles", "categories", "collaborators", "publish", "logs"]; + + let iframeLoaded = $state(false); @@ -55,7 +57,15 @@
{#if fullPreview} - + {#if !iframeLoaded} +

Loading preview...

+ {/if} + {:else} {@html md( previewContent.value || "Write some markdown content to see a live preview here", diff --git a/web-app/src/routes/(anonymous)/login/+page.server.ts b/web-app/src/routes/(anonymous)/login/+page.server.ts index e31083e..d3f382c 100644 --- a/web-app/src/routes/(anonymous)/login/+page.server.ts +++ b/web-app/src/routes/(anonymous)/login/+page.server.ts @@ -18,7 +18,7 @@ export const actions: Actions = { return response; } - cookies.set("session_token", response.data.token, { path: "/", maxAge: 86400 }); + cookies.set("session_token", response.data.token, { path: "/", maxAge: 43200 }); return response; } }; diff --git a/web-app/src/routes/(authenticated)/website/[websiteId]/publish/+page.server.ts b/web-app/src/routes/(authenticated)/website/[websiteId]/publish/+page.server.ts index fd46ae9..f606e7e 100644 --- a/web-app/src/routes/(authenticated)/website/[websiteId]/publish/+page.server.ts +++ b/web-app/src/routes/(authenticated)/website/[websiteId]/publish/+page.server.ts @@ -269,14 +269,15 @@ const generateStaticFiles = async ( }; const setPermissions = async (dir: string) => { - await chmod(dir, 0o777); + const mode = dev ? 0o777 : process.env.ORIGIN ? 0o770 : 0o777; + await chmod(dir, mode); const entries = await readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = join(dir, entry.name); if (entry.isDirectory()) { await setPermissions(fullPath); } else { - await chmod(fullPath, 0o777); + await chmod(fullPath, mode); } } };