Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 2026-07-25 - [Sentinel: Path Traversal/Zip Slip in Uploaded Filenames via Backslashes]
**Vulnerability:** Path traversal (Zip Slip) vulnerability due to incomplete filename sanitization on POSIX servers (CWE-22 / CWE-29).
**Learning:** On POSIX systems, `pathlib.Path(filename).name` does not treat backslashes (`\`) as directory separators. A client can supply a filename like `..\..\windows.ini`, bypassing the intended basename extraction and potentially allowing path traversal or overwriting files during extraction if the resulting filename is used to create an archive.
**Prevention:** Explicitly normalize directory separators by replacing all backslashes with forward slashes (`filename.replace('\\', '/')`) before passing the string to path manipulation libraries like `pathlib.Path().name`.

## 2026-05-28 - [Sentinel Fixes: Temp Files & Injection]
**Vulnerability:** Predictable Temp Files (CWE-377) and Insecure Default Permissions (CWE-276), plus Command Injection via FFmpeg Filtergraph (CWE-20).
**Learning:** Python's `Path.with_name` plus a suffix string to make a temp file opens a race condition because it's predictable and the permissions default to system `umask` which might expose secret `0600` data. Additionally, interpolating variables directly into FFmpeg filtergraph strings allows arbitrary filter injection.
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@

### Fixed
- 단일·일괄 대상 크기 입력을 비웠을 때 이전 custom validity와 `aria-invalid` 상태를 즉시 초기화해 현재 필수 입력 상태를 정확히 전달합니다.
- [보안] 업로드된 파일명 내 백슬래시(`\`)가 디렉토리 구분자로 처리되지 않아 발생할 수 있는 경로 탐색 및 Zip Slip 취약점(CWE-22)을 수정했습니다.
4 changes: 2 additions & 2 deletions saas_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ def _persist_upload(file: UploadFile) -> tuple[Path, Path, Path, Path]:
input_dir.mkdir()
output_dir.mkdir()

safe_filename = Path(file.filename).name
safe_filename = Path((file.filename or "").replace("\\", "/")).name
if not safe_filename or safe_filename in (".", ".."):
safe_filename = "upload.tmp"

Expand Down Expand Up @@ -619,7 +619,7 @@ def shrink_media_batch(
try:
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_STORED) as archive:
for index, upload in enumerate(files):
safe_filename = Path(upload.filename or "").name
safe_filename = Path((upload.filename or "").replace("\\", "/")).name
if not safe_filename or safe_filename in (".", ".."):
safe_filename = "upload.tmp"
entry = {
Expand Down
23 changes: 23 additions & 0 deletions tests/test_saas_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,29 @@ def test_shrink_batch_handles_archive_failure(self, _mock_zipfile):
self.assertEqual(response.status_code, 500)
self.assertEqual(response.json(), {"error": "Upload processing failed"})

@patch("saas_web.media_shrinker.convert_file")
def test_shrink_batch_uses_safe_fallback_filename_with_backslashes(self, mock_convert_file):
mock_convert_file.return_value = []

response = saas_web.shrink_media_batch(
BackgroundTasks(),
files=[
SimpleNamespace(
filename="..\\..\\windows.ini",
content_type="audio/wav",
file=io.BytesIO(b"dummy"),
)
],
target_bytes=10000,
)

archive = zipfile.ZipFile(response.path)
manifest = json.loads(archive.read("results.json"))
self.assertEqual(manifest["results"][0]["filename"], "windows.ini")
self.assertEqual(
mock_convert_file.call_args.kwargs["source"].name, "windows.ini"
)

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
@patch("saas_web.media_shrinker.convert_file")
def test_shrink_batch_uses_safe_fallback_filename(self, mock_convert_file):
mock_convert_file.return_value = []
Expand Down
Loading