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 ---