Add test and security baseline

- make quick/check/mutation from cj/security-baseline (Makefile unchanged,
  settings in baseline.mk), pre-commit, gitleaks, Semgrep rules, ESLint
  security rules, Forgejo workflow
- tests/security: generic baseline tests plus app config and role tests
  against createApp() with an in-memory database
- extract createApp() from src/server/index.ts; LB_DATABASE_PATH for tests
- fix gaps found by the tests: reject unknown fields on project create,
  answer malformed JSON with 400 instead of 500, set nosniff on the API
- npm audit fix (next 16.4.1 and others, no high/critical left)
- clear ESLint errors, safeFileName without control-character regex
- per-project ownership stays an open, documented gap (idorGap -> TODO)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Grovy311 2026-10-15 09:40:33 +02:00
parent 67476ea59b
commit a7a1595cbf
38 changed files with 5852 additions and 442 deletions

72
security-mutations.toml Normal file
View file

@ -0,0 +1,72 @@
# Sicherheits-Mutationen der Leistungsbilanz: jede baut gezielt eine Schutzfunktion aus und
# muss mindestens einen inhaltlich passenden Test rot machen. Lauf: make mutation
# Läuft nur die Sicherheitstests (schnell); Node im node:24-Container, falls lokal kein npm.
test_cmd = ["sh", "-c", "if command -v npm >/dev/null; then node --import tsx --test tests/security/baseline.test.ts tests/security/roles.test.ts tests/authentication.test.ts; else docker run --rm -u $(id -u):$(id -g) -e HOME=/tmp -v \"$PWD\":/w -w /w node:24 node --import tsx --test tests/security/baseline.test.ts tests/security/roles.test.ts tests/authentication.test.ts; fi"]
workdir = "."
[[mutation]]
name = "Login-Pflicht für /api entfernt"
file = "src/server/app.ts"
old = " app.use(\"/api\", createAuthGuard(authConfig, sessionStore, userRepository));\n"
new = "\n"
[[mutation]]
name = "Neue ungeschützte Route vor dem Auth-Guard"
file = "src/server/app.ts"
old = " app.use(\"/api\", requireSameOriginForWrites());"
new = " app.get(\"/api/debug\", (_req, res) => {\n res.json({ ok: true });\n });\n app.use(\"/api\", requireSameOriginForWrites());"
[[mutation]]
name = "Admin-Rolle für /api/users entfernt"
file = "src/server/routes/user.routes.ts"
old = "userRouter.use(requireRole(\"admin\"));"
new = ""
[[mutation]]
name = "Passwortwechsel-Pflicht entfernt"
file = "src/server/app.ts"
old = " app.use(\"/api\", requirePasswordChanged());\n"
new = "\n"
[[mutation]]
name = "CSRF-Header-Prüfung entfernt"
file = "src/server/auth/auth.middleware.ts"
old = " if (req.get(WRITE_REQUEST_HEADER_NAME) !== WRITE_REQUEST_HEADER_VALUE) {"
new = " if (false) {"
[[mutation]]
name = "Unbekannte Felder erlaubt (Mass Assignment)"
file = "src/shared/validation/project-structure.schemas.ts"
old = " threePhaseVoltageV: z.number().positive().optional(),\n}).strict();"
new = " threePhaseVoltageV: z.number().positive().optional(),\n});"
[[mutation]]
name = "Kaputtes JSON führt wieder zu 500"
file = "src/server/middleware/error.middleware.ts"
old = " if (status !== undefined) {"
new = " if (false) {"
[[mutation]]
name = "Fehlertext an Client durchgereicht"
file = "src/server/middleware/error.middleware.ts"
old = " res.status(500).json({ error: \"Internal Server Error\" });"
new = " res.status(500).json({ error: String((error as Error).stack) });"
[[mutation]]
name = "CORS für alle Origins"
file = "src/server/app.ts"
old = " app.disable(\"x-powered-by\");"
new = " app.disable(\"x-powered-by\");\n app.use((_req, res, next) => {\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n next();\n });"
[[mutation]]
name = "nosniff-Header entfernt"
file = "src/server/app.ts"
old = " res.setHeader(\"X-Content-Type-Options\", \"nosniff\");"
new = ""
[[mutation]]
name = "X-Powered-By wieder an"
file = "src/server/app.ts"
old = " app.disable(\"x-powered-by\");"
new = ""