Skip to content

Commit 87a46aa

Browse files
authored
Merge pull request #4426 from songoow/codex/public-safety-drive-qualified-paths
fix(public-safety): recognize drive-qualified, UNC and /data paths and quoted secrets
2 parents 65fadb6 + 30b7342 commit 87a46aa

3 files changed

Lines changed: 99 additions & 11 deletions

File tree

‎loopx/control_plane/runtime/public_safety.py‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,21 @@
1010
DEFAULT_PUBLIC_SAFE_LIST_LIMIT = 4
1111
LOCAL_PATH_SURFACE_PATTERN = re.compile(
1212
r"(?<![:/A-Za-z0-9])(?:"
13-
r"/(?:Users|home|Volumes|private|tmp|var|etc|opt|srv|mnt|root|workspace|workspaces)/"
13+
r"/(?:Users|home|Volumes|private|tmp|var|etc|opt|srv|mnt|root|data|workspace|workspaces)/"
1414
r"[^\s`'\"<>]+|"
15-
r"[A-Za-z]:[\\/](?:Users|Documents and Settings)[\\/][^\s`'\"<>]+"
15+
r"[A-Za-z]:[\\/][^\s`'\"<>]+|"
16+
r"\\\\[A-Za-z0-9_.-]+\\[^\s`'\"<>]+"
1617
r")",
1718
re.IGNORECASE,
1819
)
1920
SECRET_LIKE_SURFACE_PATTERN = re.compile(
2021
r"(?i)(?:\bbearer\s+[a-z0-9._~+/=-]{16,}|"
21-
r"\b(?:access|secret)[_-]?key\s*[=:]\s*[^\s`'\"<>]+|"
22-
r"\b(?:ak|sk)\s*[=:]\s*[^\s`'\"<>]+|"
22+
r"\b(?:access|secret)[_-]?key[\"']?\s*[=:]\s*[\"']?[^\s`'\"<>]+|"
23+
r"\b(?:ak|sk)[\"']?\s*[=:]\s*[\"']?[^\s`'\"<>]+|"
2324
r"(?<![a-z0-9_])(?:ak|sk)[-_=:][a-z0-9_=-]{10,}|"
2425
r"\bgh[pousr]_[a-z0-9]{20,}\b|"
2526
r"\beyj[a-z0-9_-]{10,}\.[a-z0-9_-]{10,}\.[a-z0-9_-]{10,}\b|"
26-
r"\btoken\s*[=:]\s*[^\s`'\"<>]{12,})"
27+
r"\btoken[\"']?\s*[=:]\s*[\"']?[^\s`'\"<>]{12,})"
2728
)
2829
_CREDENTIAL_FIELD_FAMILIES = frozenset(
2930
{
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""Boundary shapes for the shared public-safety surface rules.
2+
3+
The local-path rule recognizes every drive-qualified path, UNC shares and
4+
``/data`` roots; the secret rule tolerates quoted keys and values. Neither
5+
widening may start matching URLs, clock times or ratios.
6+
"""
7+
8+
import pytest
9+
10+
from loopx.control_plane.runtime.public_safety import (
11+
LOCAL_PATH_SURFACE_PATTERN,
12+
SECRET_LIKE_SURFACE_PATTERN,
13+
validate_public_safe_value,
14+
)
15+
16+
REJECTED_PATHS = [
17+
"C:" + chr(92) + "build" + chr(92) + "evidence.txt",
18+
"C:/workspace/private/worker.json",
19+
"D:" + chr(92) + "Projects" + chr(92) + "loopx" + chr(92) + "state.json",
20+
chr(92) * 2 + "server" + chr(92) + "share" + chr(92) + "evidence.txt",
21+
"/data/reports/evidence.txt",
22+
"C:" + chr(92) + "Users" + chr(92) + "fixture" + chr(92) + "evidence.txt",
23+
]
24+
25+
ACCEPTED_TEXT = [
26+
"https://example.org/data/report",
27+
"s3://bucket/key",
28+
"notion://page/1",
29+
"12:30/45",
30+
"ratio 3:4/5 done",
31+
"id x:y/z",
32+
"docs/evidence.md",
33+
"access key rotation guide",
34+
]
35+
36+
def _secret(key: str, separator: str, quote: str = "", key_quote: str = "") -> str:
37+
"""Build a credential-shaped probe without a literal secret assignment."""
38+
39+
return f"{key_quote}{key}{key_quote}{separator}{quote}{'synthetic' * 4}{quote}"
40+
41+
42+
REJECTED_SECRETS = [
43+
_secret("token", ": ", quote='"'),
44+
_secret("token", "="),
45+
_secret("access_key", "=", quote="'"),
46+
_secret("access_key", ": ", quote='"', key_quote='"'),
47+
_secret("sk", " = ", quote="'"),
48+
]
49+
50+
51+
@pytest.mark.parametrize("value", REJECTED_PATHS)
52+
def test_drive_unc_and_data_paths_are_local_paths(value):
53+
assert LOCAL_PATH_SURFACE_PATTERN.search(value)
54+
with pytest.raises(ValueError, match="absolute local path"):
55+
validate_public_safe_value(value)
56+
57+
58+
@pytest.mark.parametrize("value", ACCEPTED_TEXT)
59+
def test_urls_times_and_ratios_are_not_local_paths(value):
60+
assert not LOCAL_PATH_SURFACE_PATTERN.search(value)
61+
assert not SECRET_LIKE_SURFACE_PATTERN.search(value)
62+
validate_public_safe_value(value)
63+
64+
65+
@pytest.mark.parametrize("value", REJECTED_SECRETS)
66+
def test_quoted_secret_keys_and_values_are_credential_like(value):
67+
assert SECRET_LIKE_SURFACE_PATTERN.search(value)
68+
with pytest.raises(ValueError, match="credential-like"):
69+
validate_public_safe_value(value)
70+
71+
72+
def test_path_shaped_value_reports_local_path_before_opaque_shape():
73+
# The turn-executor contract records this ordering: a drive-qualified
74+
# path is a local path first, so that diagnostic wins over the
75+
# opaque-reference shape check that runs afterwards.
76+
with pytest.raises(ValueError, match="absolute local path"):
77+
validate_public_safe_value({"worker_ref": "C:/workspace/private/worker.json"})

‎tests/test_loopx_turn_executor.py‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -756,15 +756,27 @@ def test_enabled_host_result_rejects_receipt_local_path() -> None:
756756

757757

758758
@pytest.mark.parametrize(
759-
("field", "value"),
759+
("field", "value", "expected_error"),
760760
[
761-
("worker_ref", "C:/workspace/private/worker.json"),
762-
("evidence_refs", ["file:/tmp/private-result.json"]),
761+
# A drive-qualified path is now recognized as a local path, so the
762+
# shared public-safety rule reports it before the opaque-shape check.
763+
# Both rules reject the value; only the diagnostic differs.
764+
(
765+
"worker_ref",
766+
"C:/workspace/private/worker.json",
767+
"contains an absolute local path",
768+
),
769+
(
770+
"evidence_refs",
771+
["file:/tmp/private-result.json"],
772+
"opaque 1-192 character public-safe reference",
773+
),
763774
],
764775
)
765776
def test_enabled_host_result_rejects_path_shaped_opaque_refs(
766777
field: str,
767778
value: object,
779+
expected_error: str,
768780
) -> None:
769781
plan = _adaptive_observation_plan()
770782
result = _host_result(plan)
@@ -775,9 +787,7 @@ def test_enabled_host_result_rejects_path_shaped_opaque_refs(
775787
rejected = validate_loopx_turn_host_result(plan, result)
776788

777789
assert rejected["ok"] is False
778-
assert "opaque 1-192 character public-safe reference" in " ".join(
779-
rejected["errors"]
780-
)
790+
assert expected_error in " ".join(rejected["errors"])
781791
assert "child_execution_receipts" not in rejected["result"]
782792
rejected_value = value[0] if isinstance(value, list) else value
783793
assert rejected_value not in json.dumps(

0 commit comments

Comments
 (0)