fix: refuse the sandbox rather than quietly running it as the superuser
Both reviews landed on the same line independently. _as_role swaps the credentials in the DSN with a regex, and when the pattern does not match it returned the string unchanged. Two shapes miss it: no inline credentials, and a unix socket URL. Either one is a legal DSN. What that costs is not a broken test. The sandbox builds, every assertion still passes, and bare_dsn is now the admin connection, so the worst-case case runs the real script with --apply as a superuser against the shared table. The verifier ran that command as a dry run to see what it would have done: target public.llm_calls, 11 rows to delete. The case would still have gone red on the exit code, after the rows were gone. It raises now. There is also a second check that connects and compares current_user, because a successful string substitution is not the same as connecting as that role -- PGUSER and friends still override. The whole design rests on that connection having no grant on the shared table; a string comparison is too thin a thing to rest it on. That check has to stay inside the try. Past it the cleanup statements have already been merged into the fixture-level stack, and unwinding again runs DROP OWNED BY twice, which has no IF EXISTS. The catalog probe took any SQL and ran it on the admin connection. The design claims withholding the DSN makes the boundary structural; that was only true of the connection string, not of the capability. It takes SELECT now. --table's schema half is restricted to plain identifiers. Not a security fix, since the name goes through a parameter and _quote: the help text says complex identifiers are unsupported and the code was accepting them anyway.
This commit is contained in:
@@ -19,6 +19,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import re
|
||||
import sqlite3
|
||||
import sys
|
||||
from datetime import UTC, datetime, timedelta
|
||||
@@ -30,6 +31,11 @@ if TYPE_CHECKING:
|
||||
|
||||
TABLE = "llm_calls"
|
||||
|
||||
# --table 的 schema 段白名单。收紧到普通标识符不是为了防注入(目标名走 to_regclass
|
||||
# 的参数化占位,且用 _quote 转义),而是让 --help 里"不支持复杂标识符"这句话与实现
|
||||
# 一致——文档说不支持、实现却照单全收,受害的是照文档做判断的人。
|
||||
_PLAIN_IDENTIFIER = re.compile(r"[A-Za-z_][A-Za-z0-9_$]*")
|
||||
|
||||
# 退出码是本脚本对调度器(cron/systemd)的公共契约,改动即破坏下游告警规则
|
||||
EXIT_OK = 0
|
||||
EXIT_USAGE = 1
|
||||
@@ -195,12 +201,13 @@ def _parse_table(parser: _Parser, value: str) -> str:
|
||||
if len(segments) != 2:
|
||||
parser.error(f"--table 必须是 <schema>.{TABLE} 这样的两段限定名,当前: {value!r}")
|
||||
schema, name = segments
|
||||
# 段内不可能再含 "." (上面按 "." 切成恰好两段),故此处只需查引号
|
||||
# 段内不可能再含 "." (上面按 "." 切成恰好两段),故此处只查其余形态
|
||||
if not schema or not name:
|
||||
parser.error(f"--table 的 schema 段与表名段都不得为空,当前: {value!r}")
|
||||
if '"' in schema or '"' in name:
|
||||
if not _PLAIN_IDENTIFIER.fullmatch(schema):
|
||||
parser.error(
|
||||
f"--table 不支持含引号的复杂标识符,当前: {value!r};"
|
||||
f"--table 的 schema 段只接受普通标识符(字母或下划线开头,其后字母/数字/"
|
||||
f"下划线/$),当前: {value!r};含空格、引号等需要加引号的复杂标识符不支持,"
|
||||
"这种情形请不给 --table,退回 search_path 解析那条路径。"
|
||||
)
|
||||
if name != TABLE:
|
||||
|
||||
Reference in New Issue
Block a user