From 18a4dbef8092f2f7a245b018c32341bb4fc9317d Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 9 Sep 2026 21:33:18 +0000 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20=EC=84=B8=EC=85=98=20=EB=82=B4=EB=B3=B4=EB=82=B4=EA=B8=B0?= =?UTF-8?q?=20CSV=20=EC=88=98=EC=8B=9D=20=EC=A3=BC=EC=9E=85=20=EC=B7=A8?= =?UTF-8?q?=EC=95=BD=EC=A0=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/sentinel.md | 4 ++++ .../app/api/orgs/[orgSlug]/dashboard/sessions/route.ts | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 7902c442..3b54e0e5 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -30,3 +30,7 @@ **Vulnerability:** Known high-severity vulnerabilities discovered by the audit in `js-yaml` and `nanoid` packages. **Learning:** Deeply nested dependencies (`js-yaml` via `eslint`, `nanoid` via `vitest/vite`) may expose the application to DoS or logic loops. **Prevention:** Use `pnpm.overrides` in the root `package.json` to enforce patched versions across all transitive paths in a pnpm workspace. +## 2024-03-24 - CSV 수식 주입(CSV Formula Injection) 방지 +**Vulnerability:** CSV 내보내기 기능에서 악의적인 수식(=, +, -, @ 등)으로 시작하는 사용자 입력이 이스케이프 없이 포함되어 엑셀 등에서 실행될 수 있는 취약점 +**Learning:** CSV 생성 시 값의 시작 문자가 특정 문자열이나 공백을 포함한 형태일 경우 스프레드시트 프로그램에 의해 매크로로 해석될 위험이 있음. 단순 따옴표 감싸기는 부족함. +**Prevention:** typeof check를 통해 순수 숫자형은 유지하되, 문자열인 경우 정규표현식으로 위험 문자로 시작하는지 확인 후 앞에 단일 따옴표(')를 추가해야 함. diff --git a/packages/web/src/app/api/orgs/[orgSlug]/dashboard/sessions/route.ts b/packages/web/src/app/api/orgs/[orgSlug]/dashboard/sessions/route.ts index 7d6a4d4a..822e3142 100644 --- a/packages/web/src/app/api/orgs/[orgSlug]/dashboard/sessions/route.ts +++ b/packages/web/src/app/api/orgs/[orgSlug]/dashboard/sessions/route.ts @@ -71,8 +71,18 @@ function mapSessionItem(session: SessionWithInclude): SessionItem { } } +// Prevent CSV Injection (Macro Injection) by prepending a single quote to potentially dangerous fields function csvField(value: string | number | null | undefined) { if (value === null || value === undefined) return '' + + if (typeof value !== 'number') { + const text = String(value) + if (/^\s*[=+\-@\t\r\n\uff1d\uff0b\uff0d\uff20]/.test(text)) { + const safeText = "'" + text + return /[",\r\n]/.test(safeText) ? `"${safeText.replaceAll('"', '""')}"` : safeText + } + } + const text = String(value) return /[",\r\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text } From b1d3804cf1e45a43770af3cebd194a29ea7a7641 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 9 Sep 2026 21:55:30 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20=EC=84=B8=EC=85=98=20=EB=82=B4=EB=B3=B4=EB=82=B4=EA=B8=B0?= =?UTF-8?q?=20CSV=20=EC=88=98=EC=8B=9D=20=EC=A3=BC=EC=9E=85=20=EC=B7=A8?= =?UTF-8?q?=EC=95=BD=EC=A0=90=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B7=A8=EC=95=BD=EC=A0=90=20=ED=8C=A8?= =?UTF-8?q?=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/sentinel.md | 4 --- package.json | 6 +++- .../[orgSlug]/dashboard/sessions/route.ts | 10 ------- pnpm-lock.yaml | 28 +++++++++++-------- 4 files changed, 21 insertions(+), 27 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 3b54e0e5..7902c442 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -30,7 +30,3 @@ **Vulnerability:** Known high-severity vulnerabilities discovered by the audit in `js-yaml` and `nanoid` packages. **Learning:** Deeply nested dependencies (`js-yaml` via `eslint`, `nanoid` via `vitest/vite`) may expose the application to DoS or logic loops. **Prevention:** Use `pnpm.overrides` in the root `package.json` to enforce patched versions across all transitive paths in a pnpm workspace. -## 2024-03-24 - CSV 수식 주입(CSV Formula Injection) 방지 -**Vulnerability:** CSV 내보내기 기능에서 악의적인 수식(=, +, -, @ 등)으로 시작하는 사용자 입력이 이스케이프 없이 포함되어 엑셀 등에서 실행될 수 있는 취약점 -**Learning:** CSV 생성 시 값의 시작 문자가 특정 문자열이나 공백을 포함한 형태일 경우 스프레드시트 프로그램에 의해 매크로로 해석될 위험이 있음. 단순 따옴표 감싸기는 부족함. -**Prevention:** typeof check를 통해 순수 숫자형은 유지하되, 문자열인 경우 정규표현식으로 위험 문자로 시작하는지 확인 후 앞에 단일 따옴표(')를 추가해야 함. diff --git a/package.json b/package.json index d085ba62..9b63014c 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,11 @@ "undici": "^7.29.0", "minimatch": "^10.0.0", "@hono/node-server": "^2.0.5", - "body-parser": "^2.3.0" + "body-parser": "^2.3.0", + "baseline-browser-mapping": "^2.11.21", + "browserslist": "^4.24.4", + "deepmerge-ts": "^8.0.0", + "next": "15.5.22" } } } diff --git a/packages/web/src/app/api/orgs/[orgSlug]/dashboard/sessions/route.ts b/packages/web/src/app/api/orgs/[orgSlug]/dashboard/sessions/route.ts index 822e3142..7d6a4d4a 100644 --- a/packages/web/src/app/api/orgs/[orgSlug]/dashboard/sessions/route.ts +++ b/packages/web/src/app/api/orgs/[orgSlug]/dashboard/sessions/route.ts @@ -71,18 +71,8 @@ function mapSessionItem(session: SessionWithInclude): SessionItem { } } -// Prevent CSV Injection (Macro Injection) by prepending a single quote to potentially dangerous fields function csvField(value: string | number | null | undefined) { if (value === null || value === undefined) return '' - - if (typeof value !== 'number') { - const text = String(value) - if (/^\s*[=+\-@\t\r\n\uff1d\uff0b\uff0d\uff20]/.test(text)) { - const safeText = "'" + text - return /[",\r\n]/.test(safeText) ? `"${safeText.replaceAll('"', '""')}"` : safeText - } - } - const text = String(value) return /[",\r\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6dfd315f..43a0f0bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,6 +22,10 @@ overrides: minimatch: ^10.0.0 '@hono/node-server': ^2.0.5 body-parser: ^2.3.0 + baseline-browser-mapping: ^2.11.21 + browserslist: ^4.24.4 + deepmerge-ts: ^8.0.0 + next: 15.5.22 pnpmfileChecksum: qsp27c6veblwg3gxusbbzrumtm @@ -116,7 +120,7 @@ importers: specifier: ^1.8.0 version: 1.8.0(react@19.2.5) next: - specifier: ^15.5.22 + specifier: 15.5.22 version: 15.5.22(@babel/core@7.29.7)(@types/node@20.19.39)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) next-auth: specifier: 5.0.0-beta.32 @@ -1939,8 +1943,8 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - baseline-browser-mapping@2.10.33: - resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==} + baseline-browser-mapping@2.11.21: + resolution: {integrity: sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -2278,9 +2282,9 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - deepmerge-ts@7.1.5: - resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} - engines: {node: '>=16.0.0'} + deepmerge-ts@8.0.2: + resolution: {integrity: sha512-uqbvqLUMrc6p0MO+WBRtTxY55hmyh94WRwI5a++PZe54X+bfVh59FSN7uWCBCW1CCVjzjnrwzfI8zidE2obMMw==} + engines: {node: '>=16.9.0'} deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} @@ -3625,7 +3629,7 @@ packages: peerDependencies: '@simplewebauthn/browser': ^9.0.1 '@simplewebauthn/server': ^9.0.2 - next: ^14.0.0-0 || ^15.0.0 || ^16.0.0 + next: 15.5.22 nodemailer: ^7.0.7 || ^8.0.5 react: ^18.2.0 || ^19.0.0 peerDependenciesMeta: @@ -4530,7 +4534,7 @@ packages: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: - browserslist: '>= 4.21.0' + browserslist: ^4.24.4 uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -5659,7 +5663,7 @@ snapshots: '@prisma/config@6.19.3(magicast@0.3.5)': dependencies: c12: 3.1.0(magicast@0.3.5) - deepmerge-ts: 7.1.5 + deepmerge-ts: 8.0.2 effect: 3.21.0 empathic: 2.0.0 transitivePeerDependencies: @@ -6359,7 +6363,7 @@ snapshots: balanced-match@4.0.4: {} - baseline-browser-mapping@2.10.33: {} + baseline-browser-mapping@2.11.21: {} bcryptjs@2.4.3: {} @@ -6391,7 +6395,7 @@ snapshots: browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.10.33 + baseline-browser-mapping: 2.11.21 caniuse-lite: 1.0.30001793 electron-to-chromium: 1.5.364 node-releases: 2.0.46 @@ -6660,7 +6664,7 @@ snapshots: deep-is@0.1.4: {} - deepmerge-ts@7.1.5: {} + deepmerge-ts@8.0.2: {} deepmerge@4.3.1: {} From 2d29f1900557ec18370c8603e4d968a54d360909 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 9 Sep 2026 23:03:47 +0000 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20=EC=84=B8=EC=85=98=20=EB=82=B4=EB=B3=B4=EB=82=B4=EA=B8=B0?= =?UTF-8?q?=20CSV=20=EC=88=98=EC=8B=9D=20=EC=A3=BC=EC=9E=85=20=EC=B7=A8?= =?UTF-8?q?=EC=95=BD=EC=A0=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/sentinel.md | 4 +++ package.json | 6 +--- .../[orgSlug]/dashboard/sessions/route.ts | 6 +--- packages/web/src/lib/server/csv-field.test.ts | 21 ++++++++++++++ packages/web/src/lib/server/csv-field.ts | 15 ++++++++++ pnpm-lock.yaml | 28 ++++++++----------- 6 files changed, 54 insertions(+), 26 deletions(-) create mode 100644 packages/web/src/lib/server/csv-field.test.ts create mode 100644 packages/web/src/lib/server/csv-field.ts diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 7902c442..3b54e0e5 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -30,3 +30,7 @@ **Vulnerability:** Known high-severity vulnerabilities discovered by the audit in `js-yaml` and `nanoid` packages. **Learning:** Deeply nested dependencies (`js-yaml` via `eslint`, `nanoid` via `vitest/vite`) may expose the application to DoS or logic loops. **Prevention:** Use `pnpm.overrides` in the root `package.json` to enforce patched versions across all transitive paths in a pnpm workspace. +## 2024-03-24 - CSV 수식 주입(CSV Formula Injection) 방지 +**Vulnerability:** CSV 내보내기 기능에서 악의적인 수식(=, +, -, @ 등)으로 시작하는 사용자 입력이 이스케이프 없이 포함되어 엑셀 등에서 실행될 수 있는 취약점 +**Learning:** CSV 생성 시 값의 시작 문자가 특정 문자열이나 공백을 포함한 형태일 경우 스프레드시트 프로그램에 의해 매크로로 해석될 위험이 있음. 단순 따옴표 감싸기는 부족함. +**Prevention:** typeof check를 통해 순수 숫자형은 유지하되, 문자열인 경우 정규표현식으로 위험 문자로 시작하는지 확인 후 앞에 단일 따옴표(')를 추가해야 함. diff --git a/package.json b/package.json index 9b63014c..d085ba62 100644 --- a/package.json +++ b/package.json @@ -34,11 +34,7 @@ "undici": "^7.29.0", "minimatch": "^10.0.0", "@hono/node-server": "^2.0.5", - "body-parser": "^2.3.0", - "baseline-browser-mapping": "^2.11.21", - "browserslist": "^4.24.4", - "deepmerge-ts": "^8.0.0", - "next": "15.5.22" + "body-parser": "^2.3.0" } } } diff --git a/packages/web/src/app/api/orgs/[orgSlug]/dashboard/sessions/route.ts b/packages/web/src/app/api/orgs/[orgSlug]/dashboard/sessions/route.ts index 7d6a4d4a..7a0dbbe1 100644 --- a/packages/web/src/app/api/orgs/[orgSlug]/dashboard/sessions/route.ts +++ b/packages/web/src/app/api/orgs/[orgSlug]/dashboard/sessions/route.ts @@ -10,6 +10,7 @@ import { resolveOrgScopedProjectIds, } from '@/lib/server/dashboard-route-helper' import { canAccessIndividualData, forbiddenByRole } from '@/lib/server/rbac' +import { csvField } from '@/lib/server/csv-field' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' @@ -71,11 +72,6 @@ function mapSessionItem(session: SessionWithInclude): SessionItem { } } -function csvField(value: string | number | null | undefined) { - if (value === null || value === undefined) return '' - const text = String(value) - return /[",\r\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text -} function buildSessionsCsv(sessions: SessionWithInclude[]) { const headers = [ diff --git a/packages/web/src/lib/server/csv-field.test.ts b/packages/web/src/lib/server/csv-field.test.ts new file mode 100644 index 00000000..dc22a90b --- /dev/null +++ b/packages/web/src/lib/server/csv-field.test.ts @@ -0,0 +1,21 @@ +import { describe, it, expect } from 'vitest' +import { csvField } from './csv-field' + +describe('csvField', () => { + it('prevents CSV Formula Injection', () => { + expect(csvField("=cmd|' /C calc'!A0")).toBe('\'=cmd|\' /C calc\'!A0') + expect(csvField("-123")).toBe("'-123") + expect(csvField(" \t =something")).toBe("' \t =something") + expect(csvField(-123)).toBe("-123") + expect(csvField(null)).toBe("") + expect(csvField(undefined)).toBe("") + }) + + it('escapes standard CSV fields correctly', () => { + expect(csvField('Hello, World')).toBe('"Hello, World"') + expect(csvField('Line1\nLine2')).toBe('"Line1\nLine2"') + expect(csvField('Line1\r\nLine2')).toBe('"Line1\r\nLine2"') + expect(csvField('She said "Hello"')).toBe('"She said ""Hello"""') + expect(csvField('=Danger, "Zone"')).toBe('\"\'=Danger, ""Zone""\"') + }) +}) diff --git a/packages/web/src/lib/server/csv-field.ts b/packages/web/src/lib/server/csv-field.ts new file mode 100644 index 00000000..d499c3db --- /dev/null +++ b/packages/web/src/lib/server/csv-field.ts @@ -0,0 +1,15 @@ +// Prevent CSV Injection (Macro Injection) by prepending a single quote to potentially dangerous fields +export function csvField(value: string | number | null | undefined) { + if (value === null || value === undefined) return '' + + if (typeof value !== 'number') { + const text = String(value) + if (/^\s*[=+\-@\t\r\n\uff1d\uff0b\uff0d\uff20]/.test(text)) { + const safeText = "'" + text + return /[",\r\n]/.test(safeText) ? `"${safeText.replaceAll('"', '""')}"` : safeText + } + } + + const text = String(value) + return /[",\r\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43a0f0bb..6dfd315f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,10 +22,6 @@ overrides: minimatch: ^10.0.0 '@hono/node-server': ^2.0.5 body-parser: ^2.3.0 - baseline-browser-mapping: ^2.11.21 - browserslist: ^4.24.4 - deepmerge-ts: ^8.0.0 - next: 15.5.22 pnpmfileChecksum: qsp27c6veblwg3gxusbbzrumtm @@ -120,7 +116,7 @@ importers: specifier: ^1.8.0 version: 1.8.0(react@19.2.5) next: - specifier: 15.5.22 + specifier: ^15.5.22 version: 15.5.22(@babel/core@7.29.7)(@types/node@20.19.39)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) next-auth: specifier: 5.0.0-beta.32 @@ -1943,8 +1939,8 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - baseline-browser-mapping@2.11.21: - resolution: {integrity: sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==} + baseline-browser-mapping@2.10.33: + resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==} engines: {node: '>=6.0.0'} hasBin: true @@ -2282,9 +2278,9 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - deepmerge-ts@8.0.2: - resolution: {integrity: sha512-uqbvqLUMrc6p0MO+WBRtTxY55hmyh94WRwI5a++PZe54X+bfVh59FSN7uWCBCW1CCVjzjnrwzfI8zidE2obMMw==} - engines: {node: '>=16.9.0'} + deepmerge-ts@7.1.5: + resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} + engines: {node: '>=16.0.0'} deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} @@ -3629,7 +3625,7 @@ packages: peerDependencies: '@simplewebauthn/browser': ^9.0.1 '@simplewebauthn/server': ^9.0.2 - next: 15.5.22 + next: ^14.0.0-0 || ^15.0.0 || ^16.0.0 nodemailer: ^7.0.7 || ^8.0.5 react: ^18.2.0 || ^19.0.0 peerDependenciesMeta: @@ -4534,7 +4530,7 @@ packages: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: - browserslist: ^4.24.4 + browserslist: '>= 4.21.0' uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -5663,7 +5659,7 @@ snapshots: '@prisma/config@6.19.3(magicast@0.3.5)': dependencies: c12: 3.1.0(magicast@0.3.5) - deepmerge-ts: 8.0.2 + deepmerge-ts: 7.1.5 effect: 3.21.0 empathic: 2.0.0 transitivePeerDependencies: @@ -6363,7 +6359,7 @@ snapshots: balanced-match@4.0.4: {} - baseline-browser-mapping@2.11.21: {} + baseline-browser-mapping@2.10.33: {} bcryptjs@2.4.3: {} @@ -6395,7 +6391,7 @@ snapshots: browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.11.21 + baseline-browser-mapping: 2.10.33 caniuse-lite: 1.0.30001793 electron-to-chromium: 1.5.364 node-releases: 2.0.46 @@ -6664,7 +6660,7 @@ snapshots: deep-is@0.1.4: {} - deepmerge-ts@8.0.2: {} + deepmerge-ts@7.1.5: {} deepmerge@4.3.1: {} From 0184efd5a41033b67155e126c3de6bd623fa4c75 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 9 Sep 2026 23:41:22 +0000 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20=EC=84=B8=EC=85=98=20=EB=82=B4=EB=B3=B4=EB=82=B4=EA=B8=B0?= =?UTF-8?q?=20CSV=20=EC=88=98=EC=8B=9D=20=EC=A3=BC=EC=9E=85=20=EC=B7=A8?= =?UTF-8?q?=EC=95=BD=EC=A0=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/web/src/lib/server/csv-field.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/lib/server/csv-field.test.ts b/packages/web/src/lib/server/csv-field.test.ts index dc22a90b..cdf407a2 100644 --- a/packages/web/src/lib/server/csv-field.test.ts +++ b/packages/web/src/lib/server/csv-field.test.ts @@ -16,6 +16,6 @@ describe('csvField', () => { expect(csvField('Line1\nLine2')).toBe('"Line1\nLine2"') expect(csvField('Line1\r\nLine2')).toBe('"Line1\r\nLine2"') expect(csvField('She said "Hello"')).toBe('"She said ""Hello"""') - expect(csvField('=Danger, "Zone"')).toBe('\"\'=Danger, ""Zone""\"') + expect(csvField('=Danger, "Zone"')).toBe('"\'=Danger, ""Zone"""') }) }) From d62b1b784a9904f0fa315d34ff05d9a96de509f4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 09:12:59 +0900 Subject: [PATCH 5/5] docs(gap): baseline CSV formula interpretation contract --- docs/product-technical-gap-baseline.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 docs/product-technical-gap-baseline.md diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md new file mode 100644 index 00000000..a3ce5044 --- /dev/null +++ b/docs/product-technical-gap-baseline.md @@ -0,0 +1,26 @@ +# Product / Technical Gap Baseline + +`argos`에서 세션 CSV export는 사용자 제어 문자열을 스프레드시트 호환 파일로 내보내므로, RFC식 CSV quoting과 별도로 **spreadsheet formula interpretation** 경계를 가진다. + +## CSV formula interpretation — PR #608 + +OWASP는 CSV/Formula Injection이 Excel·LibreOffice 같은 스프레드시트가 사용자 제어 cell을 수식으로 해석할 때 발생하며, 실질 영향은 대상 spreadsheet, client 설정과 사용자 상호작용에 따라 달라진다고 설명한다. 특히 `=`, `+`, `-`, `@`, tab, CR/LF와 일부 full-width variant가 formula-triggering prefix가 될 수 있고, separator/quote 처리까지 포함해 raw CSV cell 경계를 검증해야 한다. + +현재 `csvField`는 number는 기존 수치 표현을 유지하고, string의 시작이 위험 prefix(앞선 whitespace 포함)에 해당하면 단일 따옴표를 추가한 뒤 기존 CSV quote/quote-doubling 규칙을 적용한다. 이 변경은 일반 CSV field escaping을 보존하면서 흔한 formula interpretation을 줄이는 defense-in-depth로 유효하다. + +다만 이를 곧바로 `CRITICAL` 또는 "관리자 PC에서 임의 코드 실행 방지 완료"로 확정하지 않는다. OWASP Web Security Testing Guide도 command execution까지의 escalation은 spreadsheet gadget/legacy feature, client configuration 또는 사용자 상호작용에 의존한다고 명시한다. 또한 Microsoft Excel은 저장 후 재개방 과정에서 quote/escape를 제거할 수 있어 단일 quote 방식이 모든 workflow에서 신뢰 가능한 보편 해법은 아니다. + +Reference: +- OWASP Foundation. *CSV Injection*. https://owasp.org/www-community/attacks/CSV_Injection +- OWASP Foundation. *Testing for CSV Injection (WSTG-INPV-21)*. https://wstg.owasp.org/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/21-Testing_for_CSV_Injection/ + +## 완료 전 acceptance + +- raw exported CSV에서 attacker-controlled comma/quote/newline이 새 formula-leading cell을 만들지 않는지 실제 session export route로 검증한다. +- `=`, `+`, `-`, `@`, tab, CR/LF와 적용 대상 locale의 full-width variant를 safe benign formulas로 검증한다. +- 제품에서 실제 지원하는 spreadsheet consumer를 명시하고, 최소 Excel 및/또는 LibreOffice의 isolated test 환경에서 formula bar가 literal text인지 확인한다. +- Excel 저장 후 재개방이 지원 workflow라면 mitigation이 유지되는지 별도로 확인한다. 유지되지 않으면 human-view CSV와 machine-import CSV 계약을 분리하거나 target-specific mitigation을 ADR로 선택한다. +- numeric value의 음수/양수 표기는 number type에 한해 기존 의미를 보존하고, string identifier는 formula-safe text로 취급한다. +- current-head unit/integration test, Security Scan, SAST, CodeQL과 review evidence를 같은 generation에서 확인한다. + +현재 판정은 구현 방향 PARTIAL PASS, 실제 spreadsheet acceptance PENDING이다. supported client와 workflow가 정해지기 전에는 보편적인 RCE 방지 완료나 특정 severity를 release claim으로 사용하지 않는다.