alert-ack: значение секрета вебхука GlitchTip больше не пишется в лог — строка доступа и ошибки разбора дают secret=*** #3588
2 changed files with 63 additions and 1 deletions
|
|
@ -21,7 +21,11 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import logging
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
from http.server import ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
|
|
@ -179,3 +183,48 @@ def test_protuhshiy_token_ne_prinimaetsya(app: ModuleType, monkeypatch: pytest.M
|
|||
code, _ = app.do_ack(token)
|
||||
assert code == 404
|
||||
assert app.sent == []
|
||||
|
||||
|
||||
# ── Секрет из query не попадает в лог (#3576) ────────────────────────────────
|
||||
# GlitchTip шлёт секрет резервного вебхука только в `?secret=`, а http.server
|
||||
# печатает строку запроса целиком — и в строке доступа, и в тексте ошибки
|
||||
# разбора. Значение ниже выдуманное: проверяется, что его нет ни в одной записи.
|
||||
|
||||
_LEAK = "leak-probe-3576-VALUE"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("request_line", "masked", "lines_with_mask"),
|
||||
[
|
||||
# Строка доступа (log_request), путь резервного вебхука GlitchTip.
|
||||
(f"POST /glitchtip?secret={_LEAK} HTTP/1.1", "/glitchtip?secret=***", 1),
|
||||
# Имя с префиксом и соседний параметр — остальная строка цела.
|
||||
(f"GET /ack/x?a=1&access_token={_LEAK}&b=2 HTTP/1.1", "?a=1&access_token=***&b=2", 1),
|
||||
# Ошибка разбора (log_error): stdlib кладёт строку запроса в текст ошибки,
|
||||
# затем та же строка идёт в строку доступа с кодом 400 — обе записи.
|
||||
(f"POST /glitchtip?secret={_LEAK} junk HTTP/1.1", "/glitchtip?secret=***", 2),
|
||||
],
|
||||
)
|
||||
def test_sekret_iz_query_ne_popadaet_v_log(
|
||||
app: ModuleType,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
request_line: str,
|
||||
masked: str,
|
||||
lines_with_mask: int,
|
||||
) -> None:
|
||||
caplog.set_level(logging.INFO, logger="alert-ack")
|
||||
srv = ThreadingHTTPServer(("127.0.0.1", 0), app.Handler)
|
||||
threading.Thread(target=srv.serve_forever, daemon=True).start()
|
||||
try:
|
||||
with socket.create_connection(srv.server_address, timeout=5) as sock:
|
||||
sock.sendall(f"{request_line}\r\nHost: x\r\nContent-Length: 0\r\n\r\n".encode())
|
||||
sock.shutdown(socket.SHUT_WR)
|
||||
while sock.recv(4096): # до закрытия: к этому моменту запись лога уже сделана
|
||||
pass
|
||||
finally:
|
||||
srv.shutdown()
|
||||
srv.server_close()
|
||||
|
||||
messages = [r.getMessage() for r in caplog.records if r.name == "alert-ack"]
|
||||
assert not [m for m in messages if _LEAK in m], f"значение секрета в логе: {messages}"
|
||||
assert sum(masked in m for m in messages) == lines_with_mask, messages
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ import html
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import threading
|
||||
import time
|
||||
|
|
@ -76,6 +77,16 @@ TTL_SEC = int(os.environ.get("ALERT_ACK_TTL_MIN", "1440")) * 60
|
|||
GLITCHTIP_SECRET = os.environ.get("ALERT_ACK_GLITCHTIP_SECRET", "")
|
||||
API = "https://api.telegram.org/bot{}/{}"
|
||||
|
||||
# Значения секретных query-параметров в логе — `***` (#3576). GlitchTip шлёт
|
||||
# секрет только в `?secret=`, а строка запроса целиком уходит в access-log. То
|
||||
# же выражение, что у бэкенда МЕРЫ (tradein-mvp/backend/app/core/log_scrub.py,
|
||||
# #3154) и у Alloy (#3354); импортировать нельзя — сервис без зависимостей.
|
||||
_SENSITIVE_QUERY = re.compile(
|
||||
r"([?&][\w.-]*(?:secret|token|api[-_]?key|apikey|access[-_]?token|password|signature|sig)=)"
|
||||
r"[^&\s\"'<>]+",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
# token -> {"message_id": int, "title": str, "created": float, "acked_by": str|None}
|
||||
_PENDING: dict[str, dict] = {}
|
||||
_LOCK = threading.Lock()
|
||||
|
|
@ -319,7 +330,9 @@ class Handler(BaseHTTPRequestHandler):
|
|||
protocol_version = "HTTP/1.1"
|
||||
|
||||
def log_message(self, fmt: str, *args) -> None: # noqa: A003 — подпись из stdlib
|
||||
log.info("%s %s", self.address_string(), fmt % args)
|
||||
# Сюда сходятся и строка доступа (log_request), и ошибки разбора запроса
|
||||
# (log_error: «Bad request syntax ('POST /glitchtip?secret=…')») — маскируем здесь.
|
||||
log.info("%s %s", self.address_string(), _SENSITIVE_QUERY.sub(r"\1***", fmt % args))
|
||||
|
||||
def _reply(self, code: int, body: bytes, ctype: str = "text/html; charset=utf-8") -> None:
|
||||
self.send_response(code)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue