fix(est): reject oversized attachments early

This commit is contained in:
2026-11-07 11:23:39 +09:00
parent d2a8889475
commit 20ef5a6a7f
6 changed files with 45 additions and 5 deletions
+2 -1
View File
@@ -2,7 +2,7 @@ const fs = require("node:fs");
const path = require("node:path");
const vm = require("node:vm");
const base = path.resolve(__dirname, "../../tools/est-test");
const context = vm.createContext({ structuredClone, console });
const context = vm.createContext({ structuredClone, console, Blob });
for (const file of [
"native-math.js",
"native-formula.js",
@@ -19,6 +19,7 @@ for (const file of [
"native-resources.js",
"native-schedule.js",
"native-issues.js",
"native-attachments.js",
"native-model.js",
"native-c3r.js",
"native-exchange.js",
+15
View File
@@ -3,6 +3,21 @@ import subprocess
from pathlib import Path
def test_attachment_size_prediction_includes_utf8_and_base64_overhead():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();p.name='한글';const file={name:'파일.bin',type:'',size:4};
const expected={...p,id:'0'.repeat(24),attachments:[{id:'0'.repeat(36),...file,data:'AAECAw=='}]};
return {predicted:EstAttachments.requiredBytes(p,[file]),actual:new Blob([JSON.stringify(expected)]).size,
large:EstAttachments.requiredBytes(p,[{name:'large',type:'',size:210000000}]),count:p.attachments.length};
})()""",
)
assert result["predicted"] == result["actual"]
assert result["large"] > 256 * 1024 * 1024
assert result["count"] == 0
def test_attachment_validation_keeps_valid_data_and_rejects_malformed_content():
result = run(
"math",
+3
View File
@@ -1,4 +1,7 @@
## Codex · EST 독립 테스트 앱 (2026-11-04)
- [x] 첨부파일 추가 전 256MB 저장 한도 확인 → 대형 파일 차단·기존 작업 보존 시험.
- 자체 검증: UTF-8·base64 실제 저장 크기 일치·한도 계산 포함 4개 통과. Edge에서 초과 파일 읽기 전 차단·안내 확인.
- [x] 전체 수량 반영 시 중복·예약 산출서 코드 차단 → 중복 합산 방지 시험.
- 자체 검증: 중복·빈·예약 코드 차단과 기존 수량 보존 포함 시험 19개 통과.
+1 -1
View File
@@ -18,7 +18,7 @@
저장은 이 폴더의 `data/`에 작업을 보관하고 `.estweb.json` 파일도 내려받습니다. 브라우저 임시 저장은 같은 탭에서 복원하는 용도입니다. JSON은 모든 웹 업무와 원본 압축 파일·바이너리를 보관하는 전체 저장 형식입니다. ESTX 수정 저장은 아래 지원 조건을 따릅니다.
작업모드의 `첨부파일`에서 파일을 추가·내려받기·삭제할 수 있습니다. 파일은 작업 JSON에 포함되며 이 앱의 저장 요청은 전체 256MB 이하입니다. 첨부파일이 있는 작업은 JSON으로 저장해야 하며, 첨부파일이 빠진 ESTX가 만들어지지 않도록 ESTX 저장을 차단합니다.
작업모드의 `첨부파일`에서 파일을 추가·내려받기·삭제할 수 있습니다. 파일은 작업 JSON에 포함되며 이 앱의 저장 요청은 전체 256MB 이하입니다. 제한을 초과하는 파일은 읽기 전에 추가를 차단합니다. 첨부파일이 있는 작업은 JSON으로 저장해야 하며, 첨부파일이 빠진 ESTX가 만들어지지 않도록 ESTX 저장을 차단합니다.
## 추가 입출력·문서 기능 (2026-11-06)
+1 -1
View File
@@ -485,7 +485,6 @@ const EstActions = (() => {
EstExchange.install();
EstBusinessActions.install();
EstIssues.install();
EstAttachments.install();
document.addEventListener("click", async (event) => {
try {
const marker = event.target.closest("[data-marker]");
@@ -677,6 +676,7 @@ const EstActions = (() => {
}
}
});
EstAttachments.install();
}
return { install, form };
})();
+23 -2
View File
@@ -20,6 +20,24 @@ const EstAttachments = (() => {
reader.readAsDataURL(file);
});
}
function requiredBytes(project, files) {
const metadata = files.map((file) => ({
id: "0".repeat(36),
name: file.name,
type: file.type || "",
size: file.size,
data: "",
}));
const candidate = {
...project,
id: project.id || "0".repeat(24),
attachments: [...(project.attachments || []), ...metadata],
};
return (
new Blob([JSON.stringify(candidate)]).size +
files.reduce((sum, file) => sum + 4 * Math.ceil(file.size / 3), 0)
);
}
function install() {
document.addEventListener("change", async (event) => {
const input = event.target;
@@ -28,7 +46,10 @@ const EstAttachments = (() => {
key = state.documentKey,
revision = state.revision;
try {
const files = await Promise.all([...input.files].map(read));
const selected = [...input.files];
if (requiredBytes(state.project, selected) > 256 * 1024 * 1024)
throw Error("첨부파일을 포함한 작업이 저장 한도 256MB를 초과합니다.");
const files = await Promise.all(selected.map(read));
if (key !== state.documentKey || revision !== state.revision)
throw Error("작업이 변경되었습니다. 첨부파일을 다시 추가하세요.");
EstApp.change(() => (state.project.attachments ||= []).push(...files), false);
@@ -57,5 +78,5 @@ const EstAttachments = (() => {
}
});
}
return { render, install };
return { render, install, requiredBytes };
})();