mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-11-02 06:16:10 +00:00
278 lines
10 KiB
Python
278 lines
10 KiB
Python
"""Verify no RPC credential can reach a log line.
|
|
|
|
The provider endpoints in `.env` carry their API key inside the URL, and the
|
|
HTTP clients log a full request URL at INFO, so anything that raises the root
|
|
logger to INFO prints the key — to the terminal and into `logs/`.
|
|
|
|
Silencing `httpx` and `httpcore` by name did not stop it: solana-py 0.40 swapped
|
|
httpx for httpx2 and the rename took the guard with it. Naming the clients is a
|
|
convenience, not the control; these checks prove the value is masked whichever
|
|
logger emits it.
|
|
|
|
Offline machine checks, no network and no funds moved:
|
|
|
|
1. `redact` masks a secret-bearing query parameter, URL userinfo, and an
|
|
opaque path segment — the three shapes a provider credential takes.
|
|
2. `redact` masks an exact value registered from the environment, keeping the
|
|
host so a log line still says which provider answered.
|
|
3. A logger this repo has never heard of, logging at INFO, comes out
|
|
redacted — including when the URL is a non-`str` argument, which is how
|
|
httpx2 passes it and how the first version of this guard was defeated.
|
|
4. Importing `core.client` installs the redaction: every module that logs
|
|
calls `get_logger` at import, and that is where it is installed.
|
|
5. `setup_file_logging` installs it before attaching the handler, so nothing
|
|
reaches `logs/` unredacted.
|
|
6. Every `logging.basicConfig` in `src/` and `tools/` installs it too, so the
|
|
guard does not depend on import order.
|
|
7. No script prints an endpoint environment variable raw; the hostname is the
|
|
most any of them may show.
|
|
|
|
Usage:
|
|
uv run tests/regression/verify_no_rpc_credentials_logged.py
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(PROJECT_ROOT / "src"))
|
|
|
|
from utils.logger import ( # noqa: E402
|
|
REDACTED,
|
|
install_secret_redaction,
|
|
redact,
|
|
)
|
|
|
|
SCAN_ROOTS = ("src", "tools", "cookbook")
|
|
|
|
# The variables whose values are credentials. Printing one raw is the bug.
|
|
SECRET_ENV_VARS = (
|
|
"SOLANA_PRIVATE_KEY",
|
|
"GEYSER_API_TOKEN",
|
|
"SOLANA_NODE_RPC_ENDPOINT",
|
|
"SOLANA_NODE_WSS_ENDPOINT",
|
|
"GEYSER_ENDPOINT",
|
|
)
|
|
|
|
# A print or f-string that interpolates one of those names directly. Reading
|
|
# `.hostname` off it first is allowed and is what the cookbook scripts do.
|
|
RAW_ENV_IN_OUTPUT = re.compile(
|
|
r"(?:print|logger\.\w+|logging\.\w+)\([^)]*\{("
|
|
+ "|".join(SECRET_ENV_VARS + ("RPC_ENDPOINT", "WSS_ENDPOINT"))
|
|
+ r")\}"
|
|
)
|
|
|
|
|
|
class _NotAString:
|
|
"""Stands in for httpx2's URL object: renders as a URL, is not a `str`."""
|
|
|
|
def __init__(self, value: str) -> None:
|
|
self._value = value
|
|
|
|
def __str__(self) -> str:
|
|
return self._value
|
|
|
|
|
|
class _Capture(logging.Handler):
|
|
"""Collect formatted records so a check can read what would be printed."""
|
|
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.lines: list[str] = []
|
|
|
|
def emit(self, record: logging.LogRecord) -> None:
|
|
self.lines.append(record.getMessage())
|
|
|
|
|
|
def check_redact_masks_every_credential_shape() -> bool:
|
|
"""The three ways a provider puts a key in a URL."""
|
|
cases = {
|
|
"https://example.com/?api-key=deadbeef0123": "api-key=deadbeef0123",
|
|
"https://example.com/?apiKey=deadbeef0123": "deadbeef0123",
|
|
"wss://user:hunter2@example.com/stream": "user:hunter2",
|
|
"https://example.com/abcdef0123456789abcdef/": "abcdef0123456789abcdef",
|
|
}
|
|
ok = True
|
|
for raw, secret in cases.items():
|
|
cleaned = redact(raw)
|
|
if secret in cleaned or REDACTED not in cleaned:
|
|
print(f" not masked: {raw} -> {cleaned}")
|
|
ok = False
|
|
return ok
|
|
|
|
|
|
def check_redact_masks_a_registered_value() -> bool:
|
|
"""An exact environment value is masked, and the host survives."""
|
|
endpoint = "https://rpc.example.com/9f8e7d6c5b4a39281706"
|
|
os.environ["SOLANA_NODE_RPC_ENDPOINT"] = endpoint
|
|
try:
|
|
install_secret_redaction()
|
|
cleaned = redact(f"connecting to {endpoint}")
|
|
finally:
|
|
del os.environ["SOLANA_NODE_RPC_ENDPOINT"]
|
|
install_secret_redaction()
|
|
if "9f8e7d6c5b4a39281706" in cleaned:
|
|
print(f" the registered value survived: {cleaned}")
|
|
return False
|
|
if "rpc.example.com" not in cleaned:
|
|
print(f" the host was masked too, which makes the line useless: {cleaned}")
|
|
return False
|
|
return True
|
|
|
|
|
|
def check_an_unknown_logger_is_redacted() -> bool:
|
|
"""The control, and the one that survives a library renaming its logger.
|
|
|
|
Silencing clients by name is a list that goes stale. This logs the same
|
|
line httpx2 logs, through a name nothing in this repo mentions, at INFO.
|
|
"""
|
|
install_secret_redaction()
|
|
logger = logging.getLogger("some.library.nobody.listed")
|
|
logger.setLevel(logging.INFO)
|
|
capture = _Capture()
|
|
logger.addHandler(capture)
|
|
try:
|
|
logger.info(
|
|
"HTTP Request: POST https://mainnet.example.com/?api-key=deadbeef0123 "
|
|
'"HTTP/2 200 OK"'
|
|
)
|
|
logger.info("endpoint=%s", "https://mainnet.example.com/?api-key=deadbeef0123")
|
|
# The shape that actually leaks. httpx2 passes its URL as a URL object,
|
|
# not a str, so a guard that redacts `record.msg` and then type-checks
|
|
# each argument skips the only one carrying the key. The first version
|
|
# of this guard did that and the key still reached the terminal.
|
|
logger.info(
|
|
'HTTP Request: %s %s "%s"',
|
|
"POST",
|
|
_NotAString("https://mainnet.example.com/?api-key=deadbeef0123"),
|
|
"HTTP/2 200 OK",
|
|
)
|
|
finally:
|
|
logger.removeHandler(capture)
|
|
leaked = [line for line in capture.lines if "deadbeef0123" in line]
|
|
if leaked:
|
|
print(f" {len(leaked)} of {len(capture.lines)} lines kept the key")
|
|
return False
|
|
if not all("mainnet.example.com" in line for line in capture.lines):
|
|
print(" the host was masked too, which makes the line useless")
|
|
return False
|
|
return len(capture.lines) == 3
|
|
|
|
|
|
def check_importing_src_installs_redaction() -> bool:
|
|
"""A fresh interpreter that imports the RPC client is already protected.
|
|
|
|
`core.client` is the module that talks to the endpoint, and like every
|
|
module that logs it calls `get_logger` at import, which installs the
|
|
redaction. Run out of process, because this interpreter already has it.
|
|
"""
|
|
code = (
|
|
"import sys, logging; sys.path.insert(0, 'src');"
|
|
" import core.client;"
|
|
" log = logging.getLogger('third.party');"
|
|
" log.setLevel(logging.INFO);"
|
|
" import io;"
|
|
" buf = io.StringIO();"
|
|
" h = logging.StreamHandler(buf);"
|
|
" log.addHandler(h);"
|
|
" log.info('GET https://x.example.com/?api-key=deadbeef0123');"
|
|
" print('LEAK' if 'deadbeef0123' in buf.getvalue() else 'CLEAN')"
|
|
)
|
|
result = subprocess.run( # noqa: S603
|
|
[sys.executable, "-c", code],
|
|
cwd=PROJECT_ROOT,
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
if "CLEAN" not in result.stdout:
|
|
print(f" {result.stdout.strip() or result.stderr.strip()[:200]}")
|
|
return False
|
|
return True
|
|
|
|
|
|
def check_file_logging_installs_redaction() -> bool:
|
|
"""The other way a root handler appears, and the one that writes to logs/.
|
|
|
|
A line printed to the terminal is gone when the scrollback is; a line in
|
|
`logs/` is not, and `logs/` is what gets attached to a bug report.
|
|
"""
|
|
source = (PROJECT_ROOT / "src" / "utils" / "logger.py").read_text()
|
|
body = source.split("def setup_file_logging")[1]
|
|
if "install_secret_redaction()" not in body.split("root_logger.addHandler")[0]:
|
|
print(" setup_file_logging attaches a handler before redacting")
|
|
return False
|
|
return True
|
|
|
|
|
|
def check_every_basic_config_installs_redaction() -> bool:
|
|
"""`logging.basicConfig` is what turns the leak on; it must turn the guard on too."""
|
|
offenders = []
|
|
for root in ("src", "tools"):
|
|
for path in sorted((PROJECT_ROOT / root).rglob("*.py")):
|
|
source = path.read_text()
|
|
if "logging.basicConfig" not in source:
|
|
continue
|
|
if "install_secret_redaction" not in source:
|
|
offenders.append(path.relative_to(PROJECT_ROOT))
|
|
for path in offenders:
|
|
print(f" {path} calls basicConfig without install_secret_redaction")
|
|
return not offenders
|
|
|
|
|
|
def check_no_script_prints_a_raw_endpoint() -> bool:
|
|
"""Print the hostname if you must, never the value."""
|
|
offenders = []
|
|
for root in SCAN_ROOTS:
|
|
for path in sorted((PROJECT_ROOT / root).rglob("*.py")):
|
|
for number, line in enumerate(path.read_text().splitlines(), start=1):
|
|
if RAW_ENV_IN_OUTPUT.search(line):
|
|
offenders.append(f"{path.relative_to(PROJECT_ROOT)}:{number}")
|
|
for hit in offenders:
|
|
print(f" {hit} interpolates a credential straight into output")
|
|
return not offenders
|
|
|
|
|
|
def main() -> int:
|
|
checks = [
|
|
(
|
|
"redact masks every credential shape",
|
|
check_redact_masks_every_credential_shape,
|
|
),
|
|
(
|
|
"redact masks a registered value, keeping the host",
|
|
check_redact_masks_a_registered_value,
|
|
),
|
|
("an unlisted logger is redacted too", check_an_unknown_logger_is_redacted),
|
|
(
|
|
"importing the RPC client installs the redaction",
|
|
check_importing_src_installs_redaction,
|
|
),
|
|
("file logging installs the redaction", check_file_logging_installs_redaction),
|
|
(
|
|
"every basicConfig installs the redaction",
|
|
check_every_basic_config_installs_redaction,
|
|
),
|
|
("no script prints a raw endpoint", check_no_script_prints_a_raw_endpoint),
|
|
]
|
|
failed = 0
|
|
for label, check in checks:
|
|
try:
|
|
ok = check()
|
|
except Exception as error: # noqa: BLE001 - report and continue
|
|
print(f"FAIL {label}: {type(error).__name__}: {error}")
|
|
failed += 1
|
|
continue
|
|
print(f"{'PASS' if ok else 'FAIL'} {label}")
|
|
failed += 0 if ok else 1
|
|
print(f"\n{len(checks) - failed}/{len(checks)} checks passed")
|
|
return 1 if failed else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|