fix: create the audit log 0600 in a 0700 directory and refuse symlinks (SEC-11)

This commit is contained in:
Latte committed 2026-10-14 17:18:29 +02:00
1 parent fd5e4b71f3
commit 7307214ab6
4 files changed
+138 -3

No files matched your search

+29 -2
View File
@@ -5,6 +5,8 @@ from __future__ import annotations
import contextlib
import hashlib
import json
import os
import stat
import threading
import uuid
from datetime import datetime, timezone
@@ -33,12 +35,37 @@ class AuditLogger:
"""
self.settings = get_settings()
self.log_path = log_path or self.settings.resolved_audit_log_path
self.log_path.parent.mkdir(parents=True, exist_ok=True)
# A new state directory is private (0700); an existing one is left alone.
self.log_path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
self._lock = threading.Lock()
# Held open for the process lifetime; closed in close().
self._log_file = open(self.log_path, "a+", encoding="utf-8") # noqa: SIM115
self._log_file = self._open_log()
self._last_hash = self._read_last_hash()
def _open_log(self) -> Any:
"""Open the log for append: mode 0600 when created, and never through a symlink.
Raises:
OSError: The path cannot be opened (a symlink at the path gives ``ELOOP``).
"""
flags = os.O_WRONLY | os.O_APPEND | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0)
descriptor = os.open(self.log_path, flags, 0o600)
try:
info = os.fstat(descriptor)
if not stat.S_ISREG(info.st_mode):
raise OSError(f"{self.log_path} is not a regular file")
if (
info.st_uid == getattr(os, "geteuid", lambda: info.st_uid)()
and stat.S_IMODE(info.st_mode) & 0o077
):
# An older log created under a permissive umask: logins and paths of
# every call must not stay readable by other local users.
os.fchmod(descriptor, 0o600)
return os.fdopen(descriptor, "a", encoding="utf-8")
except BaseException:
os.close(descriptor)
raise
def _read_last_hash(self) -> str:
"""Read the previous hash from the last log entry."""
try:
+4 -1
View File
@@ -8,6 +8,7 @@ actionable message on stderr (never on stdout, which carries JSON-RPC on stdio).
from __future__ import annotations
import asyncio
import errno
import logging
import sys
import time
@@ -428,7 +429,9 @@ def _audit_startup_error(settings: Settings) -> str | None:
try:
get_audit_logger()
except OSError as exc:
reason = exc.strerror or type(exc).__name__
reason = exc.strerror or str(exc) or type(exc).__name__
if exc.errno == errno.ELOOP:
reason = "the path is a symbolic link, which is refused"
return (
f"cannot open the audit log {settings.resolved_audit_log_path} for append "
f"({reason}). Fix the permissions or point AUDIT_LOG_PATH (or DATA_DIR) at a "