alert-ack: значение секрета вебхука GlitchTip больше не пишется в лог — строка доступа и ошибки разбора дают secret=*** #3588
2 changed files with 63 additions and 1 deletions
|
|
@ -21,7 +21,11 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import importlib.util
|
import importlib.util
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
import sys
|
import sys
|
||||||
|
import threading
|
||||||
|
from http.server import ThreadingHTTPServer
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
|
|
||||||
|
|
@ -179,3 +183,48 @@ def test_protuhshiy_token_ne_prinimaetsya(app: ModuleType, monkeypatch: pytest.M
|
||||||
code, _ = app.do_ack(token)
|
code, _ = app.do_ack(token)
|
||||||
assert code == 404
|
assert code == 404
|
||||||
assert app.sent == []
|
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 json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import secrets
|
import secrets
|
||||||
import threading
|
import threading
|
||||||
import time
|
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", "")
|
GLITCHTIP_SECRET = os.environ.get("ALERT_ACK_GLITCHTIP_SECRET", "")
|
||||||
API = "https://api.telegram.org/bot{}/{}"
|
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}
|
# token -> {"message_id": int, "title": str, "created": float, "acked_by": str|None}
|
||||||
_PENDING: dict[str, dict] = {}
|
_PENDING: dict[str, dict] = {}
|
||||||
_LOCK = threading.Lock()
|
_LOCK = threading.Lock()
|
||||||
|
|
@ -319,7 +330,9 @@ class Handler(BaseHTTPRequestHandler):
|
||||||
protocol_version = "HTTP/1.1"
|
protocol_version = "HTTP/1.1"
|
||||||
|
|
||||||
def log_message(self, fmt: str, *args) -> None: # noqa: A003 — подпись из stdlib
|
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:
|
def _reply(self, code: int, body: bytes, ctype: str = "text/html; charset=utf-8") -> None:
|
||||||
self.send_response(code)
|
self.send_response(code)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue