Lars Talian commited on
Commit
b41adc1
·
unverified ·
2 Parent(s): b33db9f9a22987

Merge pull request #92 from open-cybernauts/fix/issue-58-validator-shell-sanitization

Browse files
src/open_range/validator/isolation.py CHANGED
@@ -2,6 +2,8 @@
2
 
3
  from __future__ import annotations
4
 
 
 
5
  from open_range.protocols import CheckResult, ContainerSet, SnapshotSpec
6
 
7
  # Common service ports to probe for zone isolation violations.
@@ -33,10 +35,14 @@ class IsolationCheck:
33
  open_ports: list[int] = []
34
  for port in _PROBE_PORTS:
35
  try:
 
 
 
 
 
36
  output = await containers.exec(
37
  attacker_host,
38
- f"timeout 2 bash -c 'echo > /dev/tcp/{target_name}/{port}' "
39
- f"2>/dev/null && echo OPEN || echo CLOSED",
40
  )
41
  if "OPEN" in output:
42
  open_ports.append(port)
 
2
 
3
  from __future__ import annotations
4
 
5
+ import shlex
6
+
7
  from open_range.protocols import CheckResult, ContainerSet, SnapshotSpec
8
 
9
  # Common service ports to probe for zone isolation violations.
 
35
  open_ports: list[int] = []
36
  for port in _PROBE_PORTS:
37
  try:
38
+ probe_cmd = (
39
+ "timeout 2 bash -lc 'echo > /dev/tcp/\"$1\"/\"$2\"' _ "
40
+ f"{shlex.quote(target_name)} {shlex.quote(str(port))} "
41
+ "2>/dev/null && echo OPEN || echo CLOSED"
42
+ )
43
  output = await containers.exec(
44
  attacker_host,
45
+ probe_cmd,
 
46
  )
47
  if "OPEN" in output:
48
  open_ports.append(port)
tests/test_validator.py CHANGED
@@ -8,6 +8,7 @@ import pytest
8
  from open_range.protocols import (
9
  CheckResult,
10
  EvidenceItem,
 
11
  ExploitStep,
12
  FlagSpec,
13
  GoldenPathStep,
@@ -795,6 +796,36 @@ async def test_evidence_fails_on_nonzero_exit_even_when_output_present(mock_cont
795
  assert result.details["missing"][0]["location"] == "siem:/var/log/test.log"
796
 
797
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
798
  # ---------------------------------------------------------------------------
799
  # Check 5: Reward grounding
800
  # ---------------------------------------------------------------------------
@@ -1053,7 +1084,7 @@ async def test_isolation_fails_on_non_ssh_port(mock_containers):
1053
  # Only port 3306 is OPEN; everything else CLOSED.
1054
  async def exec_side_effect(container, cmd, **kwargs):
1055
  if container == "attacker" and "/dev/tcp/" in cmd:
1056
- if "/3306'" in cmd or "/3306}" in cmd:
1057
  return "OPEN"
1058
  return "CLOSED"
1059
  return ""
@@ -1065,6 +1096,38 @@ async def test_isolation_fails_on_non_ssh_port(mock_containers):
1065
  assert "db" in result.error
1066
 
1067
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1068
  # ---------------------------------------------------------------------------
1069
  # Check 7: Task feasibility
1070
  # ---------------------------------------------------------------------------
 
8
  from open_range.protocols import (
9
  CheckResult,
10
  EvidenceItem,
11
+ ExecResult,
12
  ExploitStep,
13
  FlagSpec,
14
  GoldenPathStep,
 
796
  assert result.details["missing"][0]["location"] == "siem:/var/log/test.log"
797
 
798
 
799
+ @pytest.mark.asyncio
800
+ async def test_evidence_quotes_pattern_and_location_path():
801
+ """Evidence grep command must quote pattern and path from snapshot content."""
802
+ import shlex
803
+
804
+ from open_range.validator.evidence import EvidenceCheck
805
+
806
+ class RecordingContainers:
807
+ def __init__(self) -> None:
808
+ self.calls: list[tuple[str, str]] = []
809
+
810
+ async def exec_run(self, container: str, cmd: str, **kwargs) -> ExecResult:
811
+ self.calls.append((container, cmd))
812
+ return ExecResult(stdout="1", exit_code=0)
813
+
814
+ containers = RecordingContainers()
815
+ pattern = "ERR'; touch /tmp/pwn #"
816
+ path = "/var/log/app; echo PWNED"
817
+ spec = SnapshotSpec(
818
+ evidence_spec=[
819
+ EvidenceItem(type="log_entry", location=f"siem:{path}", pattern=pattern),
820
+ ],
821
+ )
822
+
823
+ result = await EvidenceCheck().check(spec, containers) # type: ignore[arg-type]
824
+ assert result.passed is True
825
+ assert containers.calls
826
+ assert containers.calls[0][1] == f"grep -c {shlex.quote(pattern)} {shlex.quote(path)}"
827
+
828
+
829
  # ---------------------------------------------------------------------------
830
  # Check 5: Reward grounding
831
  # ---------------------------------------------------------------------------
 
1084
  # Only port 3306 is OPEN; everything else CLOSED.
1085
  async def exec_side_effect(container, cmd, **kwargs):
1086
  if container == "attacker" and "/dev/tcp/" in cmd:
1087
+ if " 3306 " in cmd:
1088
  return "OPEN"
1089
  return "CLOSED"
1090
  return ""
 
1096
  assert "db" in result.error
1097
 
1098
 
1099
+ @pytest.mark.asyncio
1100
+ async def test_isolation_uses_argument_safe_tcp_probe_for_target_name():
1101
+ """Target names are passed as positional args, not interpolated into script."""
1102
+ from open_range.validator.isolation import IsolationCheck
1103
+
1104
+ class RecordingContainers:
1105
+ def __init__(self) -> None:
1106
+ self.calls: list[tuple[str, str]] = []
1107
+
1108
+ async def exec(self, container: str, cmd: str, **kwargs) -> str:
1109
+ self.calls.append((container, cmd))
1110
+ return "CLOSED"
1111
+
1112
+ containers = RecordingContainers()
1113
+ target = "db'; touch /tmp/pwn #"
1114
+ spec = SnapshotSpec(
1115
+ topology={"hosts": ["attacker", "db"], "zones": {"internal": [target]}},
1116
+ flags=[],
1117
+ golden_path=[],
1118
+ task=TaskSpec(red_briefing="Go.", blue_briefing="Watch."),
1119
+ )
1120
+
1121
+ result = await IsolationCheck().check(spec, containers) # type: ignore[arg-type]
1122
+ assert result.passed is True
1123
+ assert containers.calls
1124
+ first_cmd = containers.calls[0][1]
1125
+ script_part, _, arg_part = first_cmd.partition(" _ ")
1126
+ assert "bash -lc 'echo > /dev/tcp/\"$1\"/\"$2\"'" in script_part
1127
+ assert "touch /tmp/pwn" not in script_part
1128
+ assert "touch /tmp/pwn" in arg_part
1129
+
1130
+
1131
  # ---------------------------------------------------------------------------
1132
  # Check 7: Task feasibility
1133
  # ---------------------------------------------------------------------------