fix(security): address multiple security findings from audit
- Fix SQLCipher PRAGMA key interpolation (hex-encode key to prevent crash on single quotes) - Enforce min password length (8 chars) on admin user creation - Add length bounds on username/display_name and login inputs - Invalidate other sessions on password change - Multi-stage Docker build (exclude build tools from runtime) - Exclude docs/ from Docker image - Consolidate dotenv.config() to single entry point - Document flat family authorization model in SECURITY.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+2
-5
@@ -9,11 +9,8 @@ node_modules
|
||||
.gitignore
|
||||
.dockerignore
|
||||
|
||||
# Documentation & screenshots (not needed at runtime)
|
||||
docs/screenshots/
|
||||
docs/superpowers/
|
||||
docs/social-preview.png
|
||||
docs/logo.svg
|
||||
# Documentation (not needed at runtime)
|
||||
docs/
|
||||
|
||||
# Tests
|
||||
test-*.js
|
||||
|
||||
@@ -7,8 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.5.3] - 2026-04-03
|
||||
|
||||
### Security
|
||||
- Fix SQLCipher PRAGMA key interpolation — encryption keys containing single quotes no longer crash on startup; key is now hex-encoded
|
||||
- Enforce minimum password length (8 characters) when admin creates new users — previously any 1-character password was accepted
|
||||
- Add length bounds on username (64 chars) and display_name (128 chars) to prevent unbounded input
|
||||
- Add input length bounds on login (username 64 chars, password 1024 chars)
|
||||
- Invalidate all other sessions when a user changes their password — previously active sessions survived password reset
|
||||
- Session and CSRF cookies now have `secure: true` by default; HTTP is only allowed when `SESSION_SECURE=false` is explicitly set in `.env` — previously cookies were sent without `Secure` flag in non-production environments
|
||||
- Document authorization model in SECURITY.md — clarify that all family members share read/write access to all data by design
|
||||
|
||||
### Changed
|
||||
- Use multi-stage Docker build to exclude build tools (python3, make, g++) from runtime image
|
||||
- Exclude `docs/` directory from Docker image via `.dockerignore`
|
||||
- Consolidate `dotenv.config()` to single call in `server/index.js` — remove duplicate calls from `server/db.js` and `server/auth.js`
|
||||
|
||||
## [0.5.2] - 2026-04-01
|
||||
|
||||
|
||||
+15
-3
@@ -1,4 +1,4 @@
|
||||
FROM node:22-slim
|
||||
FROM node:22-slim AS build
|
||||
|
||||
# SQLCipher-Abhängigkeiten
|
||||
RUN apt-get update && apt-get install -y \
|
||||
@@ -6,7 +6,6 @@ RUN apt-get update && apt-get install -y \
|
||||
make \
|
||||
g++ \
|
||||
libsqlcipher-dev \
|
||||
gosu \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
@@ -15,7 +14,20 @@ WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
# Anwendungscode
|
||||
# ---- Runtime stage ----
|
||||
FROM node:22-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libsqlcipher0 \
|
||||
gosu \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Node modules aus Build-Stage kopieren
|
||||
COPY --from=build /app/node_modules ./node_modules
|
||||
|
||||
# Anwendungscode (docs/ wird via .dockerignore ausgeschlossen)
|
||||
COPY . .
|
||||
|
||||
# Daten-Volume-Verzeichnis anlegen (Permissions werden zur Laufzeit gesetzt)
|
||||
|
||||
@@ -36,6 +36,15 @@ Vulnerabilities that require physical access to the host or root on the server a
|
||||
- Optional SQLCipher AES-256 database encryption
|
||||
- No API endpoint accessible without session auth (except login)
|
||||
|
||||
## Authorization Model
|
||||
|
||||
Oikos uses a flat family authorization model:
|
||||
|
||||
- **Admin** can create, edit, and delete all user accounts and all shared data.
|
||||
- **Member** can read and write all shared data (tasks, shopping lists, meals, calendar events, notes, contacts, budget entries) but cannot manage user accounts.
|
||||
|
||||
There is no per-user data isolation — all family members see and can edit all data. This is intentional: Oikos is a shared family planner, not a multi-tenant application.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Only the latest version on `main` receives security updates. There are no LTS branches.
|
||||
|
||||
+29
-1
@@ -6,7 +6,6 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcrypt');
|
||||
const session = require('express-session');
|
||||
@@ -164,6 +163,10 @@ router.post('/login', loginLimiter, async (req, res) => {
|
||||
return res.status(400).json({ error: 'Benutzername und Passwort erforderlich.', code: 400 });
|
||||
}
|
||||
|
||||
if (username.length > 64 || password.length > 1024) {
|
||||
return res.status(400).json({ error: 'Eingabe zu lang.', code: 400 });
|
||||
}
|
||||
|
||||
const user = db.get().prepare('SELECT * FROM users WHERE username = ?').get(username);
|
||||
|
||||
if (!user) {
|
||||
@@ -279,6 +282,18 @@ router.post('/users', requireAuth, requireAdmin, csrfMiddleware, async (req, res
|
||||
return res.status(400).json({ error: 'Benutzername, Anzeigename und Passwort erforderlich.', code: 400 });
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
return res.status(400).json({ error: 'Passwort muss mindestens 8 Zeichen haben.', code: 400 });
|
||||
}
|
||||
|
||||
if (username.length > 64) {
|
||||
return res.status(400).json({ error: 'Benutzername darf maximal 64 Zeichen lang sein.', code: 400 });
|
||||
}
|
||||
|
||||
if (display_name.length > 128) {
|
||||
return res.status(400).json({ error: 'Anzeigename darf maximal 128 Zeichen lang sein.', code: 400 });
|
||||
}
|
||||
|
||||
if (!['admin', 'member'].includes(role)) {
|
||||
return res.status(400).json({ error: 'Ungültige Rolle.', code: 400 });
|
||||
}
|
||||
@@ -330,6 +345,19 @@ router.patch('/me/password', requireAuth, csrfMiddleware, async (req, res) => {
|
||||
const hash = await bcrypt.hash(new_password, 12);
|
||||
db.get().prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(hash, req.session.userId);
|
||||
|
||||
// Alle anderen Sessions dieses Users invalidieren (aktuelle behalten)
|
||||
const currentSid = req.sessionID;
|
||||
const allSessions = db.get().prepare('SELECT sid, sess FROM sessions').all();
|
||||
for (const row of allSessions) {
|
||||
if (row.sid === currentSid) continue;
|
||||
try {
|
||||
const sess = JSON.parse(row.sess);
|
||||
if (sess.userId === req.session.userId) {
|
||||
db.get().prepare('DELETE FROM sessions WHERE sid = ?').run(row.sid);
|
||||
}
|
||||
} catch { /* ignore malformed session */ }
|
||||
}
|
||||
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
console.error('[Auth] Passwort-Ändern-Fehler:', err);
|
||||
|
||||
+2
-3
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Modul: Datenbank (Database)
|
||||
* Zweck: SQLite/SQLCipher Verbindung, Schema-Migration (versioniert) und Query-Helfer
|
||||
* Abhängigkeiten: better-sqlite3, dotenv
|
||||
* Abhängigkeiten: better-sqlite3
|
||||
*
|
||||
* SQLCipher-Hinweis:
|
||||
* Verschlüsselung funktioniert nur wenn better-sqlite3 gegen SQLCipher kompiliert wurde.
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
require('dotenv').config();
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
|
||||
@@ -34,7 +33,7 @@ function init() {
|
||||
|
||||
if (DB_KEY) {
|
||||
// Nur wirksam wenn Binary gegen SQLCipher kompiliert ist (Docker)
|
||||
db.pragma(`key='${DB_KEY}'`);
|
||||
db.pragma(`key=x'${Buffer.from(DB_KEY, 'utf8').toString('hex')}'`);
|
||||
// Sicherstellen dass die Datenbank tatsächlich entschlüsselbar ist
|
||||
try {
|
||||
db.prepare('SELECT count(*) FROM sqlite_master').get();
|
||||
|
||||
Reference in New Issue
Block a user