From 8a766181ded2757ae791c9eafe6c9f43dc15e51c Mon Sep 17 00:00:00 2001 From: Contributor Date: Fri, 7 Aug 2026 17:29:47 +0000 Subject: [PATCH] fix: hash root password instead of storing in clear text (CWE-312) - Replace plain-text write of new_root_password in api_security_reset() with scrypt-hashed storage via _hash_password(), matching how the free password is already handled. - Return new_root_password in the API response so the user sees it once before it is irreversibly hashed on disk. - Teach _resolve_credential() to detect scrypt hashes and display a human-readable placeholder instead of raw hex in the Hub credentials UI. - Harden root-password-setup systemd service: if the secrets file already contains a hash, skip chpasswd so a manual restart never sets the hash as the literal login password. --- app/sovran_systemsos_web/server.py | 10 +++++++--- modules/credentials.nix | 10 +++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/sovran_systemsos_web/server.py b/app/sovran_systemsos_web/server.py index 394eec9..2a63bc4 100644 --- a/app/sovran_systemsos_web/server.py +++ b/app/sovran_systemsos_web/server.py @@ -1554,7 +1554,11 @@ def _resolve_credential(cred: dict) -> dict | None: else: return None - value = prefix + raw + suffix + # Detect scrypt hash (salt_hex:hash_hex) — passwords stored securely + if re.match(r'^[0-9a-f]{32}:[0-9a-f]{64,}$', raw): + value = "(stored securely — use Security Reset to set a new one)" + else: + value = prefix + raw + suffix result = {"label": label, "value": value, "multiline": multiline} if qrcode: @@ -5173,7 +5177,7 @@ async def api_security_reset(): try: os.makedirs("/var/lib/secrets", exist_ok=True) with open("/var/lib/secrets/root-password", "w") as f: - f.write(new_root_password) + f.write(_hash_password(new_root_password)) os.chmod("/var/lib/secrets/root-password", 0o600) except Exception as exc: errors.append(f"write root-password: {exc}") @@ -5195,7 +5199,7 @@ async def api_security_reset(): except OSError: pass # Non-fatal - return {"ok": True, "new_password": new_free_password, "errors": errors} + return {"ok": True, "new_password": new_free_password, "new_root_password": new_root_password, "errors": errors} @app.post("/api/security/verify-integrity") diff --git a/modules/credentials.nix b/modules/credentials.nix index 7bb3dbe..7f25e1e 100644 --- a/modules/credentials.nix +++ b/modules/credentials.nix @@ -111,7 +111,15 @@ in echo "$ROOT_PASS" > "$SECRET_FILE" chmod 600 "$SECRET_FILE" fi - echo "root:$(cat "$SECRET_FILE")" | chpasswd + # If the file contains a scrypt hash (salt:hash), skip chpasswd — the + # password was already set via the Hub security reset endpoint and this + # service is only re-running as a manual recovery step. + CONTENT="$(cat "$SECRET_FILE")" + if echo "$CONTENT" | grep -qE '^[0-9a-f]{32}:[0-9a-f]{64,}$'; then + echo "root-password-setup: stored value is already hashed — skipping chpasswd" >&2 + else + echo "root:$CONTENT" | chpasswd + fi ''; };