From d0f0c32e70c9dec0e575bca46d6b1004f27a4df7 Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Fri, 13 Feb 2026 15:49:30 +0400 Subject: [PATCH] fix(chat/sdk): validate cwd against sandbox prefix to fix CodeQL alert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeQL traces session_id → cwd → os.makedirs/open as uncontrolled path. Add realpath + startswith check against /tmp/copilot- prefix directly in write_transcript_to_tempfile so CodeQL recognizes the sanitization. Also resolve the prefix with realpath for macOS where /tmp → /private/tmp. --- .../api/features/chat/sdk/transcript.py | 20 +++--- .../backend/test/chat/test_transcript.py | 62 +++++++++++++------ 2 files changed, 56 insertions(+), 26 deletions(-) diff --git a/autogpt_platform/backend/backend/api/features/chat/sdk/transcript.py b/autogpt_platform/backend/backend/api/features/chat/sdk/transcript.py index 58799c30db..f5f9825454 100644 --- a/autogpt_platform/backend/backend/api/features/chat/sdk/transcript.py +++ b/autogpt_platform/backend/backend/api/features/chat/sdk/transcript.py @@ -154,6 +154,9 @@ def _sanitize_id(raw_id: str) -> str: return _SAFE_ID_RE.sub("", raw_id) +_SAFE_CWD_PREFIX = os.path.realpath("/tmp/copilot-") + + def write_transcript_to_tempfile( transcript_content: str, session_id: str, @@ -166,15 +169,16 @@ def write_transcript_to_tempfile( Returns the absolute path to the file, or ``None`` on failure. """ - try: - os.makedirs(cwd, exist_ok=True) - safe_id = _sanitize_id(session_id)[:8] - jsonl_path = os.path.join(cwd, f"transcript-{safe_id}.jsonl") + # Validate cwd is under the expected sandbox prefix (CodeQL sanitizer). + real_cwd = os.path.realpath(cwd) + if not real_cwd.startswith(_SAFE_CWD_PREFIX): + logger.warning(f"[Transcript] cwd outside sandbox: {cwd}") + return None - # Defence-in-depth: ensure the resolved path stays inside cwd - if not os.path.realpath(jsonl_path).startswith(os.path.realpath(cwd)): - logger.warning(f"[Transcript] Path escaped cwd: {jsonl_path}") - return None + try: + os.makedirs(real_cwd, exist_ok=True) + safe_id = _sanitize_id(session_id)[:8] + jsonl_path = os.path.join(real_cwd, f"transcript-{safe_id}.jsonl") with open(jsonl_path, "w") as f: f.write(transcript_content) diff --git a/autogpt_platform/backend/test/chat/test_transcript.py b/autogpt_platform/backend/test/chat/test_transcript.py index 21c9e46def..71b1fad81f 100644 --- a/autogpt_platform/backend/test/chat/test_transcript.py +++ b/autogpt_platform/backend/test/chat/test_transcript.py @@ -85,26 +85,52 @@ class TestReadTranscriptFile: class TestWriteTranscriptToTempfile: - def test_writes_file_and_returns_path(self, tmp_path): - cwd = str(tmp_path / "workspace") - result = write_transcript_to_tempfile(VALID_TRANSCRIPT, "sess-1234-abcd", cwd) - assert result is not None - assert os.path.isfile(result) - assert result.endswith(".jsonl") - with open(result) as f: - assert f.read() == VALID_TRANSCRIPT + """Tests use /tmp/copilot-* paths to satisfy the sandbox prefix check.""" - def test_creates_parent_directory(self, tmp_path): - cwd = str(tmp_path / "new" / "dir") + def test_writes_file_and_returns_path(self): + cwd = "/tmp/copilot-test-write" + try: + result = write_transcript_to_tempfile( + VALID_TRANSCRIPT, "sess-1234-abcd", cwd + ) + assert result is not None + assert os.path.isfile(result) + assert result.endswith(".jsonl") + with open(result) as f: + assert f.read() == VALID_TRANSCRIPT + finally: + import shutil + + shutil.rmtree(cwd, ignore_errors=True) + + def test_creates_parent_directory(self): + cwd = "/tmp/copilot-test-mkdir" + try: + result = write_transcript_to_tempfile(VALID_TRANSCRIPT, "sess-1234", cwd) + assert result is not None + assert os.path.isdir(cwd) + finally: + import shutil + + shutil.rmtree(cwd, ignore_errors=True) + + def test_uses_session_id_prefix(self): + cwd = "/tmp/copilot-test-prefix" + try: + result = write_transcript_to_tempfile( + VALID_TRANSCRIPT, "abcdef12-rest", cwd + ) + assert result is not None + assert "abcdef12" in os.path.basename(result) + finally: + import shutil + + shutil.rmtree(cwd, ignore_errors=True) + + def test_rejects_cwd_outside_sandbox(self, tmp_path): + cwd = str(tmp_path / "not-copilot") result = write_transcript_to_tempfile(VALID_TRANSCRIPT, "sess-1234", cwd) - assert result is not None - assert os.path.isdir(cwd) - - def test_uses_session_id_prefix(self, tmp_path): - cwd = str(tmp_path) - result = write_transcript_to_tempfile(VALID_TRANSCRIPT, "abcdef12-rest", cwd) - assert result is not None - assert "abcdef12" in os.path.basename(result) + assert result is None # --- validate_transcript ---