diff --git a/.jules/palette.md b/.jules/palette.md index a1cf208b..c544e95c 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -1,3 +1,6 @@ +## 2024-07-15 - Dynamic Human-Readable File Size Validation +**Learning:** Hardcoding client-side validation messages (like "5 GiB") based on backend limits creates a disjointed user experience and misleading errors if the backend limits change. Users may encounter validation errors that contradict the UI. For batch uploads, providing validation against both file count and total size helps prevent late rejections. +**Action:** When implementing client-side file size validation, always dynamically format the backend limit constants (e.g., `formatBinaryBytes(MAX_UPLOAD_BYTES)`) rather than hardcoding human-readable values. For batch uploads, ensure client-side validation checks the sum of all file sizes against the max upload limits and use `setCustomValidity` and `aria-invalid` to display the feedback immediately. ## 2024-07-12 - Intercepting batch form submissions for testing visual loading states **Learning:** Extending the learning from 2024-06-13, intercepting form submissions using `e.preventDefault()` via `page.evaluate()` is essential for capturing screenshot and video evidence of loading states (e.g., button disabling, spinner appearing) on forms like batch upload where the submission would normally reload the page or download an archive. **Action:** When testing visual loading states with Playwright, always inject an event listener using `page.evaluate()` to call `e.preventDefault()` on the form's `submit` event to freeze the UI in its loading state for verification. diff --git a/CHANGELOG.md b/CHANGELOG.md index ebfe94a6..06d6d774 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,3 +4,5 @@ ### Added - 다중 파일 업로드 선택 시 즉각적인 파일 개수 피드백 및 제한 초과 경고 메시지 추가 - 일괄 업로드 폼에 대상 바이트 프리셋 버튼과 총 파일 크기 미리보기를 추가하여 사용성을 개선했습니다. + +* [🎨 Palette] 클라이언트 파일 업로드 제한 에러 메시지를 하드코딩된 값에서 동적으로 계산된 값(MAX_UPLOAD_BYTES)을 사용하도록 개선하고 일괄 업로드의 총 용량 제한 검증 로직 추가 diff --git a/pr_description.txt b/pr_description.txt index 2dd31444..de9f643c 100644 --- a/pr_description.txt +++ b/pr_description.txt @@ -1,10 +1,7 @@ -**Severity**: High +💡 What: 업로드 파일 용량 초과 시 하드코딩된 '5 GiB' 에러 메시지를 제거하고, `MAX_UPLOAD_BYTES` 상수를 기반으로 동적으로 계산된 용량을 표시하도록 변경했습니다. 일괄 업로드 폼(batch input)의 경우 총 파일 크기가 백엔드 제한 용량을 초과하는지 여부도 함께 검증하여 사전에 오류를 방지합니다. -**Vulnerability**: Argument Injection via relative paths starting with a hyphen in command-line utilities. +🎯 Why: 백엔드 제한값이 변경되더라도 클라이언트 측 에러 메시지에 변경 사항이 자동으로 반영되어 사용자에게 잘못된 정보를 제공하는 문제를 막고 혼란을 최소화하기 위함입니다. 일괄 업로드 시에도 사용자가 용량 초과 오류를 서버에 제출하기 전에 미리 알 수 있도록 하여 불필요한 대기 시간을 줄였습니다. -**Impact**: Command-line utilities (like `ffprobe` and `ffmpeg` filters) interpret user input (like a file path) starting with a hyphen (e.g., `-version.wav`) as options when passed as a relative path. This could lead to a command injection when parsing maliciously crafted filenames. -Even when `ffmpeg` inputs are protected by `-i`, the output paths, as well as arguments to other utilities like `brctl` and `SetFile`, can be maliciously crafted to start with `-` and be interpreted as options if relative paths are used. +📸 Before/After: 파일 제한 용량이 하드코딩된 '5 GiB'에서 `MAX_UPLOAD_BYTES` 값을 동적으로 변환한 값(예: 5.00 GiB)으로 표시되며, 일괄 업로드 시에도 총 파일 크기 초과 여부가 올바르게 표출됩니다. -**Fix**: The file paths passed to `subprocess.run` inside `media_shrinker.py` are resolved into absolute paths using `.resolve()`. However, to prevent Strix CI scanners from falsely reporting command injection on `subprocess.run`, `str()` path wrapping is being replaced with python's `f-string`. Replaced `str(path.resolve())` with `f"{path.resolve()}"`. - -**Verification**: Ran tests to ensure regressions weren't introduced by using python's `coverage`. 100% test coverage reported. +♿ Accessibility: `setCustomValidity` 및 `aria-invalid` 상태 변경과 더불어 `aria-live` 영역에 즉각적으로 초과된 용량 에러 메시지를 주입함으로써 화면 낭독기(Screen Reader) 사용자도 용량 초과 여부를 즉시 인지할 수 있도록 접근성을 개선했습니다. diff --git a/saas_web.py b/saas_web.py index 3a7b0352..1cc229cb 100644 --- a/saas_web.py +++ b/saas_web.py @@ -230,9 +230,10 @@ async def add_security_headers(request: Request, call_next): } const text = formatBinaryBytes(file.size); if (file.size > MAX_UPLOAD_BYTES) { - input.setCustomValidity('File exceeds 5 GiB limit.'); + const limitText = formatBinaryBytes(MAX_UPLOAD_BYTES); + input.setCustomValidity('File exceeds ' + limitText + ' limit.'); input.setAttribute('aria-invalid', 'true'); - preview.innerText = 'Selected file size: ' + text + ' (exceeds 5 GiB limit)'; + preview.innerText = 'Selected file size: ' + text + ' (exceeds ' + limitText + ' limit)'; preview.style.color = '#dc3545'; return; } @@ -325,6 +326,14 @@ async def add_security_headers(request: Request, call_next): preview.style.color = '#dc3545'; return; } + if (totalSize > MAX_UPLOAD_BYTES) { + const limitText = formatBinaryBytes(MAX_UPLOAD_BYTES); + input.setCustomValidity('Total size exceeds ' + limitText + ' limit.'); + input.setAttribute('aria-invalid', 'true'); + preview.innerText = 'Selected ' + files.length + ' file(s) (' + formatBinaryBytes(totalSize) + ', exceeds ' + limitText + ' limit)'; + preview.style.color = '#dc3545'; + return; + } preview.innerText = 'Selected ' + files.length + ' file(s) (' + formatBinaryBytes(totalSize) + ')'; } diff --git a/tests/test_saas_web.py b/tests/test_saas_web.py index 57b879d1..1640a6e2 100644 --- a/tests/test_saas_web.py +++ b/tests/test_saas_web.py @@ -52,7 +52,7 @@ def test_get_ui_includes_binary_file_size_validation(self): self.assertIn("const MAX_UPLOAD_BYTES = 5 * 1024 * 1024 * 1024;", html) self.assertIn("['B', 'KiB', 'MiB', 'GiB']", html) - self.assertIn("File exceeds 5 GiB limit.", html) + self.assertIn("File exceeds ' + limitText + ' limit.", html) self.assertIn("preview.style.color = '#0f6674';", html) self.assertIn('onchange="updateFileSizePreview(this)"', html) @@ -594,6 +594,7 @@ def test_get_ui_includes_batch_upload_form(self): self.assertIn('onchange="updateBatchFilePreview(this)"', html) self.assertIn('id="batch_files_preview"', html) self.assertIn('function updateBatchFilePreview(input)', html) + self.assertIn("Total size exceeds ' + limitText + ' limit.", html) @unittest.skipUnless(_HAS_FASTAPI, "fastapi not installed (optional integration dependency)")