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.
This commit is contained in:
Contributor
2026-08-07 12:34:13 -05:00
committed by naturallaw777
parent bd0d2cd812
commit 8a766181de
2 changed files with 16 additions and 4 deletions
+6 -2
View File
@@ -1554,6 +1554,10 @@ def _resolve_credential(cred: dict) -> dict | None:
else: else:
return None return None
# 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 value = prefix + raw + suffix
result = {"label": label, "value": value, "multiline": multiline} result = {"label": label, "value": value, "multiline": multiline}
@@ -5173,7 +5177,7 @@ async def api_security_reset():
try: try:
os.makedirs("/var/lib/secrets", exist_ok=True) os.makedirs("/var/lib/secrets", exist_ok=True)
with open("/var/lib/secrets/root-password", "w") as f: 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) os.chmod("/var/lib/secrets/root-password", 0o600)
except Exception as exc: except Exception as exc:
errors.append(f"write root-password: {exc}") errors.append(f"write root-password: {exc}")
@@ -5195,7 +5199,7 @@ async def api_security_reset():
except OSError: except OSError:
pass # Non-fatal 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") @app.post("/api/security/verify-integrity")
+9 -1
View File
@@ -111,7 +111,15 @@ in
echo "$ROOT_PASS" > "$SECRET_FILE" echo "$ROOT_PASS" > "$SECRET_FILE"
chmod 600 "$SECRET_FILE" chmod 600 "$SECRET_FILE"
fi 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
''; '';
}; };