Harden sandbox: workdir-vetter alias / namespace / MRO / sys.modules gaps, network client entry points, request() URL arg, sqlite URI mode parsing

Close eight gaps Codex found on the round-60 branch (seven inline P1 plus one
review-body P1). Five are in the runtime workdir-module import vetter (a planted
workdir helper is not scanned by the outer static pass), two are in the static
network scanner, and one is the runtime sqlite URI parser.

- assigned os aliases in the vetter: the pre-pass only recorded `import os as o`
  aliases, so a workdir helper doing `import os; o = os; o.system(...)` passed
  vetting and spawned an unguarded child. Follow simple whole-module assignments
  (o = os; b = builtins; s = sys) to a fixpoint before the sink checks.

- module namespace-dict subscripts in the vetter: `os.__dict__['system'](...)`
  reached the sink because __dict__ was not a gadget and the subscript branch only
  failed closed for builtins. Fail closed on a `<module>.__dict__[...]` subscript
  for os / posix / deserializer / sys / importlib, like vars(<module>).

- module __getattribute__ / __getattr__ in the vetter: `os.__getattribute__(
  'system')(...)` reached the sink because only the builtin getattr(...) form was
  recognized. Classify bound `module.__getattribute__('name')` and unbound
  `object.__getattribute__(module, 'name')` lookups the same way as getattr.

- MRO base recovery in the vetter: `io.FileIO.__mro__[1]('/tmp/x','w')` /
  `sqlite3.Connection.__mro__[1](...)` recovered an unguarded base class because the
  helper gadget set omitted __mro__ / mro. Add both to the gadget attributes.

- sys.modules in the vetter: `sys.modules['os'].system(...)` recovered the
  guard-cached os module without an import, bypassing the denied-import path. Deny
  sys.modules access (direct attribute and getattr form) in a vetted workdir module.

- public network client entry points: `requests.api.get(...)`, `ftplib.FTP(...)`,
  and `smtplib.SMTP(...)` were not in the network prefix table, so a metadata /
  untrusted host reached through them bypassed the allowlist. Add requests.api.*,
  ftplib.FTP / FTP_TLS, and smtplib.SMTP / SMTP_SSL / LMTP (the ftplib / smtplib
  clients take a bare host), and track ftplib / smtplib import aliases.

- request(method, url) URL argument: module-level requests.request / httpx.request /
  urllib3.request (and requests.api.request) carry the URL at arg1, but the code
  passed arg0 (the HTTP method) to the host check, so the URL was never inspected.
  Read the URL from arg1 for these method-first APIs, like the client-instance
  .request() branch.

- sqlite URI mode=memory parsing: a `file:/tmp/escape.db?xmode=memory` URI was
  treated as in-memory by a substring test and skipped path confinement, but SQLite
  ignores the unknown xmode key and opens the on-disk file. Parse the query exactly
  (split on &, first occurrence of a repeated key, percent-decoded) and treat only a
  genuine mode=memory parameter as in-memory, in the runtime guard and the two static
  sqlite operand checks.

Regression coverage: TestRound61Bypasses in tests/test_sandbox_tools.py (request()
URL at arg1 for requests / httpx / urllib3 / requests.api, network client entry
points against metadata and untrusted hosts, sqlite shell xmode=memory on an
escaping absolute path, plus a benign-allowed set: trusted-host requests / api /
ftplib / smtplib, a genuine in-memory URI, and a workdir-relative db) and, in
tests/test_sandbox_runtime_backstop.py, workdir-module denials for the assigned os
alias, os.__dict__ subscript, os.__getattribute__, io.FileIO.__mro__, and
sys.modules['os'] forms, a benign os-alias helper that still imports, and the sqlite
URI xmode=memory escape denial plus a genuine mode=memory allowance.
This commit is contained in:
danielhanchen 2026-07-11 05:29:35 +00:00
commit 236e89d0c9
3 changed files with 357 additions and 32 deletions

View file

@ -587,6 +587,24 @@ def _cwd_wrapper_escapes(tokens, cmd_idx) -> bool:
return False
def _sqlite_uri_mode_is_memory(_s: str) -> bool:
"""True only when a sqlite URI query string has a genuine mode=memory parameter. SQLite splits
query parameters on ``&`` and uses the FIRST occurrence of a repeated key, so an unknown key
(``xmode=memory``) or a later ``mode=`` is NOT in-memory -- a substring test wrongly treated
``file:/tmp/escape.db?xmode=memory`` as in-memory and skipped path confinement. Percent-decodes
each key / value so a ``mode=m%65mory`` (which SQLite decodes) is still recognized."""
def _dec(_x):
return re.sub("%([0-9A-Fa-f]{2})", lambda _m: chr(int(_m.group(1), 16)), _x)
_q = _s.partition("?")[2]
for _pair in _q.split("&"):
_k, _sep, _v = _pair.partition("=")
if _dec(_k) == "mode":
return _dec(_v) == "memory"
return False
def _operand_relative_local(tok: str) -> bool:
"""A literal RELATIVE path operand that resolves under the child cwd, so it escapes the workdir
when the cwd itself escapes (paired with _cwd_wrapper_escapes). Absolute (``/x``), home (``~``),
@ -600,7 +618,7 @@ def _operand_relative_local(tok: str) -> bool:
if not _u or _u[0] in ("/", "~", "-") or "$" in _u or "`" in _u:
return False
_ul = _u.lower()
if _u == ":memory:" or _ul.startswith("file::memory:") or "mode=memory" in _ul:
if _u == ":memory:" or _ul.startswith("file::memory:") or _sqlite_uri_mode_is_memory(_ul):
return False
return True
@ -2337,7 +2355,7 @@ def _find_blocked_commands(command: str) -> set[str]:
_is_mem = (
_dbn in ("", ":memory:")
or _dblow.startswith("file::memory:")
or "mode=memory" in _dblow
or _sqlite_uri_mode_is_memory(_dblow)
)
if not _is_mem and (
_git_operand_escapes(_dbn, _local_assigns)
@ -10125,7 +10143,11 @@ def _check_signal_escape_patterns(
"requests.patch",
"requests.head",
"requests.request",
"requests.api.",
"requests.Session",
"ftplib.FTP",
"smtplib.SMTP",
"smtplib.LMTP",
"http.client.HTTPConnection",
"http.client.HTTPSConnection",
"socket.gethostbyname",
@ -10550,7 +10572,17 @@ def _check_signal_escape_patterns(
# u -> {"u": "urllib.request"}, from urllib import request as req -> {"req":
# "urllib.request"}. Without this, r.get('http://169.254.169.254/') builds fq="r.get"
# and skips every metadata / allowlist / upload check.
_NET_TOP_MODULES = ("socket", "urllib", "urllib3", "requests", "http", "httpx", "aiohttp")
_NET_TOP_MODULES = (
"socket",
"urllib",
"urllib3",
"requests",
"http",
"httpx",
"aiohttp",
"ftplib",
"smtplib",
)
_net_aliases: dict[str, str] = {}
for _n in ast.walk(tree):
if isinstance(_n, ast.Import):
@ -10577,6 +10609,23 @@ def _check_signal_escape_patterns(
"socket.getaddrinfo",
"socket.gethostbyname",
"socket.gethostbyname_ex",
# ftplib / smtplib clients take a bare host (or host= keyword), not a URL.
"ftplib.FTP",
"ftplib.FTP_TLS",
"smtplib.SMTP",
"smtplib.SMTP_SSL",
"smtplib.LMTP",
}
)
# Module-level request(method, url, ...) APIs whose URL is the SECOND positional argument
# (the first is the HTTP method), so the host is read from args[1] like the client-instance
# .request() branch -- not args[0], which is just the method string.
_NET_REQUEST_METHOD_APIS = frozenset(
{
"requests.request",
"requests.api.request",
"httpx.request",
"urllib3.request",
}
)
_NET_HOST_KWARGS = ("host",)
@ -10873,7 +10922,10 @@ def _check_signal_escape_patterns(
# host=...)). Bare-host callees (HTTPConnection, getaddrinfo) treat the literal
# arg as a host directly; everything else parses a scheme://host URL. A target
# that stays fully opaque fails closed. See _net_check_target.
a0 = node.args[0] if node.args else None
# requests.request('GET', url) / httpx.request(...) / urllib3.request(...) carry
# the URL at arg1 (arg0 is the HTTP method); every other API carries it at arg0.
_url_idx = 1 if fq in _NET_REQUEST_METHOD_APIS else 0
a0 = node.args[_url_idx] if len(node.args) > _url_idx else None
if a0 is None:
for _kw in node.keywords or []:
if _kw.arg in _NET_URL_KWARGS or _kw.arg in _NET_ADDR_KWARGS:
@ -12397,18 +12449,33 @@ try:
# same re-exported _sqlite3.connect, so wrap once and reassign every reachable attribute.
import sqlite3 as _sq3
def _sqlite_uri_pct(_s):
# Percent-decode a URI component with the captured _bi.chr / _bi.int so a sandboxed rebind
# of chr / int cannot skew the decode (SQLite decodes file:%2Ftmp%2Fx -> /tmp/x itself).
return _re.sub("%([0-9A-Fa-f]{2})", lambda _m: _bi.chr(_bi.int(_m.group(1), 16)), _s)
def _sqlite_uri_is_memory(_params):
# True only when the query has a genuine mode=memory parameter. SQLite splits query
# parameters on '&' and uses the FIRST occurrence of a repeated key, so an unknown key
# (xmode=memory) or a later mode= is NOT in-memory -- a substring test wrongly treated
# file:/tmp/escape.db?xmode=memory as in-memory and skipped path confinement.
for _pair in _params.split("&"):
_k, _sep, _v = _pair.partition("=")
if _sqlite_uri_pct(_k) == "mode":
return _sqlite_uri_pct(_v) == "memory"
return False
def _sqlite_uri_path(_body):
# Resolve a file: URI body (already stripped of the 'file:' prefix) to the concrete path
# SQLite opens, or None for an in-memory / private target. Strips a //authority and
# percent-decodes the filename (SQLite decodes file:%2Ftmp%2Fx -> /tmp/x itself), using
# the captured _bi.chr / _bi.int so a sandboxed rebind of chr/int cannot skew the decode.
# percent-decodes the filename.
_pth, _, _params = _body.partition("?")
if _pth == ":memory:" or _pth == "" or "mode=memory" in _params.lower():
if _pth == ":memory:" or _pth == "" or _sqlite_uri_is_memory(_params):
return None
if _pth.startswith("//"):
_slash = _pth.find("/", 2)
_pth = _pth[_slash:] if _slash != -1 else ""
return _re.sub("%([0-9A-Fa-f]{2})", lambda _m: _bi.chr(_bi.int(_m.group(1), 16)), _pth)
return _sqlite_uri_pct(_pth)
def _sqlite_target_path(_db, _uri):
# The concrete filesystem path to confine for a sqlite database argument, or None when it
@ -12692,14 +12759,18 @@ try:
# unguarded callable (open.__closure__[0].cell_contents, frame.f_locals['real']) or walk to
# os / builtins. Mirrors the top-level _GADGET_DUNDERS; refuse them in a workdir helper too.
_GUARD_GADGET_ATTRS = frozenset({
"__subclasses__", "__bases__", "__base__", "__globals__", "__builtins__",
"__subclasses__", "__bases__", "__base__", "__mro__", "mro", "__globals__", "__builtins__",
"__closure__", "cell_contents", "f_locals", "f_globals", "f_back", "f_builtins",
"tb_frame", "tb_next", "gi_frame", "cr_frame", "ag_frame",
"settrace", "setprofile", "_getframe", "_current_frames", "currentframe",
})
# sys attributes that reach the import machinery: mutating them removes the guard's import
# vetter so a sibling `import evil` loads unscanned.
_GUARD_IMPORT_MACHINERY = frozenset({"meta_path", "path_hooks", "path_importer_cache"})
# sys attributes that reach the import machinery: reading sys.modules recovers a guard-cached
# module (sys.modules['os']) without an import, and mutating meta_path / path_hooks /
# path_importer_cache removes the guard's import vetter so a sibling `import evil` loads
# unscanned.
_GUARD_IMPORT_MACHINERY = frozenset(
{"modules", "meta_path", "path_hooks", "path_importer_cache"}
)
def _guard_attr_root(_v):
# Base Name id of an attribute chain (os.path -> 'os'); None if not Name-rooted.
while isinstance(_v, _gast.Attribute):
@ -12744,8 +12815,45 @@ try:
_sysmod.add(_al.asname or _al.name)
elif _al.name == "importlib":
_implib.add(_al.asname or _al.name)
# Follow simple whole-module assignments (o = os; b = builtins; s = sys) so an aliased
# receiver reached only through assignment -- not `import os as o` -- is tracked too.
# Iterate to a fixpoint so a chain (o = os; p = o) is fully resolved before the sink checks.
_alias_groups = (_recv, _bi, _deser, _sysmod, _implib)
_changed = True
while _changed:
_changed = False
for _nd in _gast.walk(_tree):
if isinstance(_nd, _gast.Assign) and isinstance(_nd.value, _gast.Name):
_srcid = _nd.value.id
for _tgt in _nd.targets:
if not isinstance(_tgt, _gast.Name):
continue
for _grp in _alias_groups:
if _srcid in _grp and _tgt.id not in _grp:
_grp.add(_tgt.id)
_changed = True
# Modules whose dynamic attribute / namespace-dict access (getattr / vars) is obfuscation.
_obf = _recv | _bi | _deser | _sysmod | _implib
def _guard_dyn_attr_hit(_grecv, _gname):
# Classify a (receiver-root, attribute-name) dynamic lookup -- from getattr(recv, name)
# or recv.__getattribute__(name) -- against the guarded sink sets. A non-constant name
# (_gname is None) on a guarded receiver fails closed; a gadget dunder escapes on ANY
# receiver; a sink name is refused only on its matching guarded receiver.
if _gname is None:
return _grecv in _obf
if _gname in _GUARD_GADGET_ATTRS:
return True
if _grecv in _recv and _gname in _GUARD_EXEC_ATTRS:
return True
if _grecv in _bi and _gname in ("eval", "exec", "compile", "__import__"):
return True
if _grecv in _deser and _gname in _GUARD_DESER_ATTRS:
return True
if _grecv in _sysmod and _gname in _GUARD_IMPORT_MACHINERY:
return True
if _grecv in _implib and _gname in ("import_module", "reload", "__import__"):
return True
return False
for _nd in _gast.walk(_tree):
if isinstance(_nd, _gast.Import):
for _al in _nd.names:
@ -12871,27 +12979,41 @@ try:
and isinstance(_nd.args[1].value, str)
else None
)
if _gname is None:
if _grecv in _obf:
return True
if _guard_dyn_attr_hit(_grecv, _gname):
return True
# os.__getattribute__('system')('id') / sys.__getattr__('modules') (bound), and the
# unbound object.__getattribute__(os, 'system') / type.__getattribute__(...) forms:
# a dynamic attribute lookup that reaches a guarded sink the builtin getattr(...)
# branch and the direct-attribute checks miss. Classify it the same way.
if (
isinstance(_nd.func, _gast.Attribute)
and _nd.func.attr in ("__getattribute__", "__getattr__")
):
_baseroot = _guard_attr_root(_nd.func.value)
if _baseroot in ("object", "type") and len(_nd.args) >= 2:
_grecv = (
_guard_attr_root(_nd.args[0])
if isinstance(_nd.args[0], (_gast.Name, _gast.Attribute))
else None
)
_gnamenode = _nd.args[1]
elif (
isinstance(_nd.func.value, (_gast.Name, _gast.Attribute))
and len(_nd.args) >= 1
):
_grecv = _baseroot
_gnamenode = _nd.args[0]
else:
# An introspection / frame gadget dunder via getattr reaches an escape on
# ANY receiver -- getattr(open, '__closure__'), getattr(cell,
# 'cell_contents') recover the guard wrapper's original unguarded open --
# so reject the gadget name regardless of receiver (mirrors the direct
# attribute check below).
if _gname in _GUARD_GADGET_ATTRS:
return True
if _grecv in _recv and _gname in _GUARD_EXEC_ATTRS:
return True
if _grecv in _bi and _gname in ("eval", "exec", "compile", "__import__"):
return True
if _grecv in _deser and _gname in _GUARD_DESER_ATTRS:
return True
if _grecv in _sysmod and _gname in _GUARD_IMPORT_MACHINERY:
return True
if _grecv in _implib and _gname in (
"import_module", "reload", "__import__"):
_grecv = None
_gnamenode = None
if _grecv is not None:
_gnm = (
_gnamenode.value
if isinstance(_gnamenode, _gast.Constant)
and isinstance(_gnamenode.value, str)
else None
)
if _guard_dyn_attr_hit(_grecv, _gnm):
return True
# vars(sys) / vars(os) / vars(builtins) exposes the module namespace dict for
# indirect access (vars(sys)['meta_path'][:] = [...], vars(os)['system']).
@ -12915,6 +13037,18 @@ try:
return True
if _skey in ("eval", "exec", "compile", "__import__"):
return True
# os.__dict__['system'] / sys.__dict__['modules'] / pickle.__dict__['loads'] --
# a namespace-dict subscript reached through a guarded module's __dict__ is the
# obfuscated twin of the direct sink attribute (the attribute checks miss the
# subscript key). Fail closed wholesale, exactly like vars(<module>) above.
# builtins is handled by the key-specific branch above (its dict legitimately
# exposes many benign names), so exclude it here.
if (
isinstance(_nd.value, _gast.Attribute)
and _nd.value.attr == "__dict__"
and _guard_attr_root(_nd.value.value) in (_recv | _deser | _sysmod | _implib)
):
return True
elif isinstance(_nd, _gast.Attribute):
# An introspection / frame gadget attribute (open.__closure__[0].cell_contents,
# frame.f_locals['real'], ().__class__.__bases__[0].__subclasses__()) recovers a
@ -12940,6 +13074,12 @@ try:
# mutation in submitted code; refuse it inside a vetted workdir module too.
if _nd.attr in ("meta_path", "path_hooks", "path_importer_cache"):
return True
# sys.modules['os'].system(...) recovers a guard-cached module without an import,
# bypassing both the denied-import path and the sink-root check (the receiver is a
# subscript, not an os name). Deny access to sys.modules in a vetted workdir module.
# Require a sys root so a benign .modules attribute (torch model.modules()) is kept.
if _nd.attr == "modules" and _guard_attr_root(_nd.value) in _sysmod:
return True
return False
def _guard_under_workdir(_p):
return _p == _GUARD_WORKDIR_REAL or _p.startswith(_GUARD_WORKDIR_REAL + _os.sep)

View file

@ -2461,3 +2461,117 @@ def test_sandboxed_docstring_same_line_write_denied(tmp_path):
)
assert "sandbox:" in out or "PermissionError" in out
assert not target.exists()
@_POSIX_ONLY
def test_sandboxed_assigned_os_alias_workdir_module_denied():
# import os; o = os; o.system(...) -- a whole-module assignment alias (not `import os as o`)
# must be followed in the vetter pre-pass so the aliased sink receiver is recognized.
_assert_workdir_module_denied(
"backstop-workdir-osassign",
"evilassign",
"import os\no = os\nprint('R61_ASSIGN')\no.system('echo PWN')\n",
"R61_ASSIGN",
)
@_POSIX_ONLY
def test_sandboxed_module_dict_subscript_workdir_module_denied():
# import os; os.__dict__['system'](...) -- a namespace-dict subscript through a guarded
# module's __dict__ reaches the sink the attribute checks miss; fail closed like vars(os).
_assert_workdir_module_denied(
"backstop-workdir-osdict",
"evildict",
"import os\nprint('R61_DICT')\nos.__dict__['system']('echo PWN')\n",
"R61_DICT",
)
@_POSIX_ONLY
def test_sandboxed_module_getattribute_workdir_module_denied():
# import os; os.__getattribute__('system')(...) -- a dynamic attribute lookup via the module
# dunder reaches the sink the builtin getattr(...) branch misses.
_assert_workdir_module_denied(
"backstop-workdir-osgetattr",
"evilga",
"import os\nprint('R61_GA')\nos.__getattribute__('system')('echo PWN')\n",
"R61_GA",
)
@_POSIX_ONLY
def test_sandboxed_mro_recovery_workdir_module_denied():
# io.FileIO.__mro__[1](...) recovers an unguarded base class in a vetted workdir helper; the
# vetter must treat __mro__ / mro as a gadget.
_assert_workdir_module_denied(
"backstop-workdir-mro",
"evilmro",
"import io\nprint('R61_MRO')\nc = io.FileIO.__mro__[1]\n",
"R61_MRO",
)
@_POSIX_ONLY
def test_sandboxed_sys_modules_subscript_workdir_module_denied():
# import sys; sys.modules['os'].system(...) recovers the guard-cached os module without an
# import, bypassing the denied-import path; access to sys.modules must be denied.
_assert_workdir_module_denied(
"backstop-workdir-sysmods",
"evilsysmods",
"import sys\nprint('R61_SYSMODS')\nsys.modules['os'].system('echo PWN')\n",
"R61_SYSMODS",
)
@_POSIX_ONLY
def test_sandboxed_benign_os_alias_workdir_module_allowed():
# A benign whole-module alias that only calls a NON-sink os attribute (o = os; o.getcwd())
# must still import -- the alias-following must not over-block ordinary os use.
session = "backstop-workdir-benignalias"
workdir = get_sandbox_workdir(session)
with open(os.path.join(workdir, "okalias.py"), "w") as f:
f.write("import os\no = os\nCWD = o.getcwd()\nprint('OKALIAS_' + 'BODY')\n")
try:
out = _python_exec(
"import okalias; print('IMPORTED_' + 'OK')",
None,
30,
session,
disable_sandbox = False,
)
assert "IMPORTED_OK" in out
assert "sandbox:" not in out
finally:
os.remove(os.path.join(workdir, "okalias.py"))
@_POSIX_ONLY
def test_sandboxed_sqlite_uri_xmode_memory_escape_denied(tmp_path):
# file:<path>?xmode=memory is an on-disk file (SQLite ignores the unknown xmode key), so the
# in-memory skip must NOT apply -- an escaping path via a uri connection is confined.
target = tmp_path / "sqlite_uri_escape.db"
out = _python_exec(
f"import sqlite3\nsqlite3.connect('file:{target}?xmode=memory', uri=True)\nprint('OPENED')",
None,
30,
"backstop-sqlite-uri-xmode",
disable_sandbox = False,
)
assert "sandbox:" in out or "PermissionError" in out
assert not target.exists()
@_POSIX_ONLY
def test_sandboxed_sqlite_uri_real_memory_allowed():
# A genuine mode=memory URI parameter is a real in-memory database and must stay allowed.
out = _python_exec(
"import sqlite3\n"
"c = sqlite3.connect('file:r61mem?mode=memory&cache=shared', uri=True)\n"
"c.execute('create table t(x)')\nprint('MEM_OK')",
None,
30,
"backstop-sqlite-uri-mem",
disable_sandbox = False,
)
assert "MEM_OK" in out
assert "sandbox:" not in out

View file

@ -5931,3 +5931,74 @@ class TestRound60Bypasses:
)
def test_round60_benign_allowed(self, code):
_ok(code)
class TestRound61Bypasses:
# Module-level request(method, url) APIs carry the URL at arg1 (arg0 is the HTTP method), so
# the host must be read from the second argument -- not the method string.
@pytest.mark.parametrize(
"code",
[
"import requests\nrequests.request('GET', 'http://169.254.169.254/')",
"import httpx\nhttpx.request('GET', 'http://169.254.169.254/')",
"import urllib3\nurllib3.request('GET', 'http://169.254.169.254/')",
"import requests\nrequests.api.request('GET', 'http://169.254.169.254/')",
],
)
def test_request_method_url_at_arg1_blocked(self, code):
_blocked(code, expect_phrase = "cloud-metadata host")
# Public network client entry points (requests.api.*, ftplib, smtplib) were not in the prefix
# table, so a metadata / untrusted host reached through them bypassed the allowlist.
@pytest.mark.parametrize(
"code",
[
"import requests\nrequests.api.get('http://169.254.169.254/')",
"import ftplib\nftplib.FTP('169.254.169.254')",
"import ftplib\nftplib.FTP_TLS('169.254.169.254')",
"import ftplib\nftplib.FTP(host='169.254.169.254')",
"import smtplib\nsmtplib.SMTP('169.254.169.254')",
"import smtplib\nsmtplib.SMTP_SSL('169.254.169.254')",
],
)
def test_network_client_entry_points_metadata_blocked(self, code):
_blocked(code, expect_phrase = "cloud-metadata host")
@pytest.mark.parametrize(
"code",
[
"import ftplib\nftplib.FTP('untrusted.example')",
"import smtplib\nsmtplib.SMTP('untrusted.example', 587)",
"import requests\nrequests.api.get('http://untrusted.example/')",
],
)
def test_network_client_entry_points_untrusted_blocked(self, code):
_blocked(code, expect_phrase = "not in sandbox allowlist")
# A sqlite operand whose absolute path carries an UNKNOWN query key that merely contains the
# text mode=memory (?xmode=memory) is still an on-disk file, so the escaping path must block --
# the in-memory skip only applies to a genuine first mode=memory parameter.
@pytest.mark.parametrize(
"cmd",
[
"sqlite3 '/tmp/escape.db?xmode=memory' 'create table t(x)'",
"sqlite3 '/tmp/escape.db?cache=shared&xmode=memory' 'create table t(x)'",
],
)
def test_sqlite_shell_xmode_memory_blocked(self, cmd):
_blocked(_sh(cmd), expect_phrase = "blocked command")
@pytest.mark.parametrize(
"code",
[
# Benign round-61 forms stay allowed.
"import requests\nrequests.request('GET', 'https://huggingface.co/x')",
"import requests\nrequests.api.get('https://huggingface.co/x')",
"import ftplib\nftplib.FTP('huggingface.co')",
"import smtplib\nsmtplib.SMTP('huggingface.co')",
_sh("sqlite3 '/tmp/x?mode=memory' 'create table t(x)'"), # genuine in-memory URI
_sh("sqlite3 'local.db' 'create table t(x)'"), # workdir-relative db
],
)
def test_round61_benign_allowed(self, code):
_ok(code)