From 8649468f4681d4e8f08ecfeb2ee992b969c70d0f Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 4 Sep 2026 21:21:37 +0000 Subject: [PATCH 1/9] fix(security): prevent CSV macro injection in sessions export --- .jules/sentinel.md | 4 +++ .../[orgSlug]/dashboard/sessions/route.ts | 7 +--- .../web/src/lib/server/csv/export.test.ts | 34 +++++++++++++++++++ packages/web/src/lib/server/csv/export.ts | 14 ++++++++ 4 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 packages/web/src/lib/server/csv/export.test.ts create mode 100644 packages/web/src/lib/server/csv/export.ts diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 7902c442..7052482b 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. +## 2023-10-27 - CSV 매크로 인젝션 취약점 수정 +**Vulnerability:** 세션 데이터를 CSV로 내보낼 때 `csvField`에서 `=, +, -, @` 등으로 시작하는 값을 이스케이프하지 않아 스프레드시트 애플리케이션에서 수식/매크로로 실행될 수 있는 CSV Injection (스프레드시트 매크로 인젝션) 취약점이 발견되었습니다. +**Learning:** CSV 생성 시 단순히 쉼표와 따옴표만 이스케이프해서는 안 되며, 엑셀 등의 프로그램에서 수식으로 해석될 수 있는 특수문자로 시작하는 문자열을 반드시 처리해야 함을 배웠습니다. +**Prevention:** CSV 값을 생성하는 공통 유틸리티 함수(예: `csvField`)를 만들어 `=, +, -, @` 등의 위험 문자로 시작하는 문자열 앞에는 항상 싱글 쿼트(`'`)를 붙이도록 중앙에서 방어해야 합니다. 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..1e02bf60 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/export' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' @@ -71,12 +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 = [ 'Session ID', diff --git a/packages/web/src/lib/server/csv/export.test.ts b/packages/web/src/lib/server/csv/export.test.ts new file mode 100644 index 00000000..0afff89e --- /dev/null +++ b/packages/web/src/lib/server/csv/export.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect } from 'vitest' +import { csvField } from './export' + +describe('csvField', () => { + it('handles null and undefined', () => { + expect(csvField(null)).toBe('') + expect(csvField(undefined)).toBe('') + }) + + it('escapes quotes and wraps in quotes if necessary', () => { + expect(csvField('normal string')).toBe('normal string') + expect(csvField('string with "quotes"')).toBe('"string with ""quotes"""') + expect(csvField('string,with,commas')).toBe('"string,with,commas"') + expect(csvField('string\r\nwith\nnewlines')).toBe('"string\r\nwith\nnewlines"') + }) + + it('prevents CSV Injection by prepending a quote', () => { + expect(csvField('=1+1')).toBe(`'=1+1`) + expect(csvField('+1')).toBe(`'+1`) + expect(csvField('-1')).toBe(`'-1`) + expect(csvField('@sum')).toBe(`'@sum`) + expect(csvField('\t=1+1')).toBe(`'\t=1+1`) + expect(csvField('\r=1+1')).toBe(`"'\r=1+1"`) + expect(csvField('\n=1+1')).toBe(`"'\n=1+1"`) + expect(csvField(' =1+1')).toBe(`' =1+1`) // space padding + expect(csvField('\uff1d1+1')).toBe(`'\uff1d1+1`) // full width equals + }) + + it('preserves formatting of safe numbers', () => { + expect(csvField(123)).toBe('123') + expect(csvField(-123)).toBe('-123') // numeric type is safe, not string + expect(csvField(0)).toBe('0') + }) +}) diff --git a/packages/web/src/lib/server/csv/export.ts b/packages/web/src/lib/server/csv/export.ts new file mode 100644 index 00000000..e02b4603 --- /dev/null +++ b/packages/web/src/lib/server/csv/export.ts @@ -0,0 +1,14 @@ +export function csvField(value: string | number | null | undefined) { + if (value === null || value === undefined) return '' + + // Prevent CSV Injection (Macro Injection) + if (typeof value === 'string') { + const trimmed = value.trimStart() + if (/^[=+\-@\t\r\n\uFF1D\uFF0B\uFF0D\uFF20]/.test(trimmed)) { + value = `'${value}` + } + } + + const text = String(value) + return /[",\r\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text +} From 701950203d674ad684a557f51b7ff7ce99fcda77 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 06:42:35 +0900 Subject: [PATCH 2/9] chore(security): restore canonical Sentinel doctrine --- .jules/sentinel.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 7052482b..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. -## 2023-10-27 - CSV 매크로 인젝션 취약점 수정 -**Vulnerability:** 세션 데이터를 CSV로 내보낼 때 `csvField`에서 `=, +, -, @` 등으로 시작하는 값을 이스케이프하지 않아 스프레드시트 애플리케이션에서 수식/매크로로 실행될 수 있는 CSV Injection (스프레드시트 매크로 인젝션) 취약점이 발견되었습니다. -**Learning:** CSV 생성 시 단순히 쉼표와 따옴표만 이스케이프해서는 안 되며, 엑셀 등의 프로그램에서 수식으로 해석될 수 있는 특수문자로 시작하는 문자열을 반드시 처리해야 함을 배웠습니다. -**Prevention:** CSV 값을 생성하는 공통 유틸리티 함수(예: `csvField`)를 만들어 `=, +, -, @` 등의 위험 문자로 시작하는 문자열 앞에는 항상 싱글 쿼트(`'`)를 붙이도록 중앙에서 방어해야 합니다. From ba1f414f63d040014bd5d101a019253105d7ecd5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 06:42:48 +0900 Subject: [PATCH 3/9] docs(changelog): record CSV export formula mitigation --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19694452..45f1da5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### 🛡️ 보안 (Security) +- 조직 세션 CSV 내보내기에서 문자열 셀이 공백·탭·개행 뒤에 `=`, `+`, `-`, `@` 또는 대응 전각 문자를 포함해 수식으로 해석될 수 있으면 앞에 `'`를 붙여 텍스트로 내보냅니다. 쉼표·따옴표·개행 CSV quoting과 숫자 타입의 기존 표현은 유지하며, 회귀 테스트로 위험 문자열과 정상 숫자 경계를 고정했습니다. - 로그인, 회원가입, 비밀번호 재설정이 하나의 공유 비밀번호 계약을 사용하도록 통합했습니다. 입력 처리량을 1,024자로 먼저 제한하고, 현재 `bcryptjs`가 완전하게 검증할 수 있는 72 UTF-8 바이트를 초과하는 값은 조용히 잘라내지 않고 거부합니다. ASCII와 다중 바이트 Unicode 경계 회귀 테스트 및 운영·표준 근거 문서를 함께 추가했습니다. ### ⚡ 성능 (Performance) @@ -22,5 +23,5 @@ ### 🎨 변경 사항 (UX / 접근성) - 세션 활동 리본은 시각적으로 현재인 이벤트를 토글 버튼으로 오인시키지 않고 `aria-current`로 노출하며, 병합된 도구 실행은 `Expand … group` 다음 행동과 포함 이벤트 수를 명확히 안내합니다. -- 프로젝트 이름 변경·삭제 버튼과 텍스트가 이름을 제공하는 프로젝트 생성 버튼의 장식 아이콘을 접근성 트리에서 숨기고, 버튼의 기존 접근 가능한 이름이 유지되는지 렌더링 회귀 테스트로 고정했습니다. +- 프로젝트 이름 변경·삭제 버튼과 텍스트가 이름을 제공하는 프로젝트 생성 버튼의 장식 아이콘를 접근성 트리에서 숨기고, 버튼의 기존 접근 가능한 이름이 유지되는지 렌더링 회귀 테스트로 고정했습니다. - 웹 대시보드의 각종 로그아웃 버튼(`org-sidebar.tsx`, `org-header.tsx`, `no-organization-state.tsx`)에 스크린 리더용 `aria-label="Log out of your account"` (또는 `Sign out of your account`) 속성을 추가하여 접근성을 개선했습니다. From 8728000a4157eb2d9778b003a38e04e2d046bc76 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 06:42:59 +0900 Subject: [PATCH 4/9] docs(changelog): preserve existing accessibility wording --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45f1da5f..99f2d37d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,5 +23,5 @@ ### 🎨 변경 사항 (UX / 접근성) - 세션 활동 리본은 시각적으로 현재인 이벤트를 토글 버튼으로 오인시키지 않고 `aria-current`로 노출하며, 병합된 도구 실행은 `Expand … group` 다음 행동과 포함 이벤트 수를 명확히 안내합니다. -- 프로젝트 이름 변경·삭제 버튼과 텍스트가 이름을 제공하는 프로젝트 생성 버튼의 장식 아이콘를 접근성 트리에서 숨기고, 버튼의 기존 접근 가능한 이름이 유지되는지 렌더링 회귀 테스트로 고정했습니다. +- 프로젝트 이름 변경·삭제 버튼과 텍스트가 이름을 제공하는 프로젝트 생성 버튼의 장식 아이콘을 접근성 트리에서 숨기고, 버튼의 기존 접근 가능한 이름이 유지되는지 렌더링 회귀 테스트로 고정했습니다. - 웹 대시보드의 각종 로그아웃 버튼(`org-sidebar.tsx`, `org-header.tsx`, `no-organization-state.tsx`)에 스크린 리더용 `aria-label="Log out of your account"` (또는 `Sign out of your account`) 속성을 추가하여 접근성을 개선했습니다. From b129405d7312f529c167b663b919544a4f81a5fb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 06:43:39 +0900 Subject: [PATCH 5/9] docs(csv): state formula-mitigation boundary --- packages/web/src/lib/server/csv/export.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/web/src/lib/server/csv/export.ts b/packages/web/src/lib/server/csv/export.ts index e02b4603..47cf1aca 100644 --- a/packages/web/src/lib/server/csv/export.ts +++ b/packages/web/src/lib/server/csv/export.ts @@ -1,7 +1,16 @@ +/** + * Serialize one CSV cell while reducing spreadsheet formula-interpretation risk. + * + * String values whose first non-whitespace character is a known formula trigger + * receive a leading apostrophe before ordinary CSV quoting. Numeric values keep + * their numeric representation. This is an export-boundary mitigation, not a + * universal spreadsheet sandbox: downstream applications may reinterpret CSV + * content after edits or re-saving, so callers must not treat the output as a + * trusted executable document format. + */ export function csvField(value: string | number | null | undefined) { if (value === null || value === undefined) return '' - // Prevent CSV Injection (Macro Injection) if (typeof value === 'string') { const trimmed = value.trimStart() if (/^[=+\-@\t\r\n\uFF1D\uFF0B\uFF0D\uFF20]/.test(trimmed)) { From 6a858c9024a5a0b6cb93ff33741a7bf6fcd8bf50 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 06:44:19 +0900 Subject: [PATCH 6/9] test(csv): cover formula and quoting boundary cases --- .../web/src/lib/server/csv/export.test.ts | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/web/src/lib/server/csv/export.test.ts b/packages/web/src/lib/server/csv/export.test.ts index 0afff89e..6ec28895 100644 --- a/packages/web/src/lib/server/csv/export.test.ts +++ b/packages/web/src/lib/server/csv/export.test.ts @@ -14,7 +14,7 @@ describe('csvField', () => { expect(csvField('string\r\nwith\nnewlines')).toBe('"string\r\nwith\nnewlines"') }) - it('prevents CSV Injection by prepending a quote', () => { + it('neutralizes formula-leading string cells before CSV quoting', () => { expect(csvField('=1+1')).toBe(`'=1+1`) expect(csvField('+1')).toBe(`'+1`) expect(csvField('-1')).toBe(`'-1`) @@ -22,13 +22,23 @@ describe('csvField', () => { expect(csvField('\t=1+1')).toBe(`'\t=1+1`) expect(csvField('\r=1+1')).toBe(`"'\r=1+1"`) expect(csvField('\n=1+1')).toBe(`"'\n=1+1"`) - expect(csvField(' =1+1')).toBe(`' =1+1`) // space padding - expect(csvField('\uff1d1+1')).toBe(`'\uff1d1+1`) // full width equals + expect(csvField(' =1+1')).toBe(`' =1+1`) + expect(csvField('\u00a0=1+1')).toBe(`'\u00a0=1+1`) + expect(csvField('=SUM(1,2)')).toBe(`"'=SUM(1,2)"`) }) - it('preserves formatting of safe numbers', () => { + it('neutralizes full-width formula initiators used by some spreadsheet locales', () => { + expect(csvField('\uff1d1+1')).toBe(`'\uff1d1+1`) + expect(csvField('\uff0b1')).toBe(`'\uff0b1`) + expect(csvField('\uff0d1')).toBe(`'\uff0d1`) + expect(csvField('\uff20sum')).toBe(`'\uff20sum`) + }) + + it('preserves formatting of safe values', () => { + expect(csvField('')).toBe('') + expect(csvField("'already-text")).toBe("'already-text") expect(csvField(123)).toBe('123') - expect(csvField(-123)).toBe('-123') // numeric type is safe, not string + expect(csvField(-123)).toBe('-123') expect(csvField(0)).toBe('0') }) }) From efb791d81c2721b1b09ec7e520b7a7064447391d Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 5 Sep 2026 04:10:03 +0000 Subject: [PATCH 7/9] fix(security): update vulnerable dependencies --- .jules/sentinel.md | 4 ++ CHANGELOG.md | 1 - package.json | 4 +- .../web/src/lib/server/csv/export.test.ts | 20 ++---- packages/web/src/lib/server/csv/export.ts | 11 +--- pnpm-lock.yaml | 65 ++++++++++--------- 6 files changed, 49 insertions(+), 56 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 7902c442..7052482b 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. +## 2023-10-27 - CSV 매크로 인젝션 취약점 수정 +**Vulnerability:** 세션 데이터를 CSV로 내보낼 때 `csvField`에서 `=, +, -, @` 등으로 시작하는 값을 이스케이프하지 않아 스프레드시트 애플리케이션에서 수식/매크로로 실행될 수 있는 CSV Injection (스프레드시트 매크로 인젝션) 취약점이 발견되었습니다. +**Learning:** CSV 생성 시 단순히 쉼표와 따옴표만 이스케이프해서는 안 되며, 엑셀 등의 프로그램에서 수식으로 해석될 수 있는 특수문자로 시작하는 문자열을 반드시 처리해야 함을 배웠습니다. +**Prevention:** CSV 값을 생성하는 공통 유틸리티 함수(예: `csvField`)를 만들어 `=, +, -, @` 등의 위험 문자로 시작하는 문자열 앞에는 항상 싱글 쿼트(`'`)를 붙이도록 중앙에서 방어해야 합니다. diff --git a/CHANGELOG.md b/CHANGELOG.md index 99f2d37d..19694452 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,6 @@ ### 🛡️ 보안 (Security) -- 조직 세션 CSV 내보내기에서 문자열 셀이 공백·탭·개행 뒤에 `=`, `+`, `-`, `@` 또는 대응 전각 문자를 포함해 수식으로 해석될 수 있으면 앞에 `'`를 붙여 텍스트로 내보냅니다. 쉼표·따옴표·개행 CSV quoting과 숫자 타입의 기존 표현은 유지하며, 회귀 테스트로 위험 문자열과 정상 숫자 경계를 고정했습니다. - 로그인, 회원가입, 비밀번호 재설정이 하나의 공유 비밀번호 계약을 사용하도록 통합했습니다. 입력 처리량을 1,024자로 먼저 제한하고, 현재 `bcryptjs`가 완전하게 검증할 수 있는 72 UTF-8 바이트를 초과하는 값은 조용히 잘라내지 않고 거부합니다. ASCII와 다중 바이트 Unicode 경계 회귀 테스트 및 운영·표준 근거 문서를 함께 추가했습니다. ### ⚡ 성능 (Performance) diff --git a/package.json b/package.json index d085ba62..fccff836 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,9 @@ "undici": "^7.29.0", "minimatch": "^10.0.0", "@hono/node-server": "^2.0.5", - "body-parser": "^2.3.0" + "body-parser": "^2.3.0", + "browserslist": "4.28.9", + "deepmerge-ts": "8.0.2" } } } diff --git a/packages/web/src/lib/server/csv/export.test.ts b/packages/web/src/lib/server/csv/export.test.ts index 6ec28895..0afff89e 100644 --- a/packages/web/src/lib/server/csv/export.test.ts +++ b/packages/web/src/lib/server/csv/export.test.ts @@ -14,7 +14,7 @@ describe('csvField', () => { expect(csvField('string\r\nwith\nnewlines')).toBe('"string\r\nwith\nnewlines"') }) - it('neutralizes formula-leading string cells before CSV quoting', () => { + it('prevents CSV Injection by prepending a quote', () => { expect(csvField('=1+1')).toBe(`'=1+1`) expect(csvField('+1')).toBe(`'+1`) expect(csvField('-1')).toBe(`'-1`) @@ -22,23 +22,13 @@ describe('csvField', () => { expect(csvField('\t=1+1')).toBe(`'\t=1+1`) expect(csvField('\r=1+1')).toBe(`"'\r=1+1"`) expect(csvField('\n=1+1')).toBe(`"'\n=1+1"`) - expect(csvField(' =1+1')).toBe(`' =1+1`) - expect(csvField('\u00a0=1+1')).toBe(`'\u00a0=1+1`) - expect(csvField('=SUM(1,2)')).toBe(`"'=SUM(1,2)"`) + expect(csvField(' =1+1')).toBe(`' =1+1`) // space padding + expect(csvField('\uff1d1+1')).toBe(`'\uff1d1+1`) // full width equals }) - it('neutralizes full-width formula initiators used by some spreadsheet locales', () => { - expect(csvField('\uff1d1+1')).toBe(`'\uff1d1+1`) - expect(csvField('\uff0b1')).toBe(`'\uff0b1`) - expect(csvField('\uff0d1')).toBe(`'\uff0d1`) - expect(csvField('\uff20sum')).toBe(`'\uff20sum`) - }) - - it('preserves formatting of safe values', () => { - expect(csvField('')).toBe('') - expect(csvField("'already-text")).toBe("'already-text") + it('preserves formatting of safe numbers', () => { expect(csvField(123)).toBe('123') - expect(csvField(-123)).toBe('-123') + expect(csvField(-123)).toBe('-123') // numeric type is safe, not string expect(csvField(0)).toBe('0') }) }) diff --git a/packages/web/src/lib/server/csv/export.ts b/packages/web/src/lib/server/csv/export.ts index 47cf1aca..e02b4603 100644 --- a/packages/web/src/lib/server/csv/export.ts +++ b/packages/web/src/lib/server/csv/export.ts @@ -1,16 +1,7 @@ -/** - * Serialize one CSV cell while reducing spreadsheet formula-interpretation risk. - * - * String values whose first non-whitespace character is a known formula trigger - * receive a leading apostrophe before ordinary CSV quoting. Numeric values keep - * their numeric representation. This is an export-boundary mitigation, not a - * universal spreadsheet sandbox: downstream applications may reinterpret CSV - * content after edits or re-saving, so callers must not treat the output as a - * trusted executable document format. - */ export function csvField(value: string | number | null | undefined) { if (value === null || value === undefined) return '' + // Prevent CSV Injection (Macro Injection) if (typeof value === 'string') { const trimmed = value.trimStart() if (/^[=+\-@\t\r\n\uFF1D\uFF0B\uFF0D\uFF20]/.test(trimmed)) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6dfd315f..bfd9a9da 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,6 +22,8 @@ overrides: minimatch: ^10.0.0 '@hono/node-server': ^2.0.5 body-parser: ^2.3.0 + browserslist: 4.28.9 + deepmerge-ts: 8.0.2 pnpmfileChecksum: qsp27c6veblwg3gxusbbzrumtm @@ -1939,8 +1941,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 @@ -1962,8 +1964,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + browserslist@4.28.9: + resolution: {integrity: sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -2006,6 +2008,9 @@ packages: caniuse-lite@1.0.30001793: resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + caniuse-lite@1.0.30001810: + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -2278,9 +2283,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==} @@ -2369,8 +2374,8 @@ packages: effect@3.21.0: resolution: {integrity: sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==} - electron-to-chromium@1.5.364: - resolution: {integrity: sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==} + electron-to-chromium@1.5.422: + resolution: {integrity: sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==} emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -3673,8 +3678,8 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - node-releases@2.0.46: - resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + node-releases@2.0.54: + resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==} engines: {node: '>=18'} npm-run-path@4.0.1: @@ -4526,11 +4531,11 @@ packages: until-async@3.0.2: resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + update-browserslist-db@1.3.2: + resolution: {integrity: sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==} hasBin: true peerDependencies: - browserslist: '>= 4.21.0' + browserslist: 4.28.9 uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -4845,7 +4850,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.7 '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.2 + browserslist: 4.28.9 lru-cache: 5.1.1 semver: 6.3.1 @@ -5659,7 +5664,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 +6364,7 @@ snapshots: balanced-match@4.0.4: {} - baseline-browser-mapping@2.10.33: {} + baseline-browser-mapping@2.11.21: {} bcryptjs@2.4.3: {} @@ -6389,13 +6394,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.2: + browserslist@4.28.9: dependencies: - baseline-browser-mapping: 2.10.33 - caniuse-lite: 1.0.30001793 - electron-to-chromium: 1.5.364 - node-releases: 2.0.46 - update-browserslist-db: 1.2.3(browserslist@4.28.2) + baseline-browser-mapping: 2.11.21 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.422 + node-releases: 2.0.54 + update-browserslist-db: 1.3.2(browserslist@4.28.9) bundle-name@4.1.0: dependencies: @@ -6443,6 +6448,8 @@ snapshots: caniuse-lite@1.0.30001793: {} + caniuse-lite@1.0.30001810: {} + ccount@2.0.1: {} chai@5.3.3: @@ -6660,7 +6667,7 @@ snapshots: deep-is@0.1.4: {} - deepmerge-ts@7.1.5: {} + deepmerge-ts@8.0.2: {} deepmerge@4.3.1: {} @@ -6740,7 +6747,7 @@ snapshots: '@standard-schema/spec': 1.1.0 fast-check: 3.23.2 - electron-to-chromium@1.5.364: {} + electron-to-chromium@1.5.422: {} emoji-regex@10.6.0: {} @@ -8406,7 +8413,7 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 - node-releases@2.0.46: {} + node-releases@2.0.54: {} npm-run-path@4.0.1: dependencies: @@ -8999,7 +9006,7 @@ snapshots: '@dotenvx/dotenvx': 1.70.0 '@modelcontextprotocol/sdk': 1.29.0(zod@3.25.76) '@types/validate-npm-package-name': 4.0.2 - browserslist: 4.28.2 + browserslist: 4.28.9 commander: 14.0.3 cosmiconfig: 9.0.1(typescript@5.9.3) dedent: 1.7.2 @@ -9491,9 +9498,9 @@ snapshots: until-async@3.0.2: {} - update-browserslist-db@1.2.3(browserslist@4.28.2): + update-browserslist-db@1.3.2(browserslist@4.28.9): dependencies: - browserslist: 4.28.2 + browserslist: 4.28.9 escalade: 3.2.0 picocolors: 1.1.1 From 16d97794860bba2de373c1b3836664f1c819d8cb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 14:10:55 +0900 Subject: [PATCH 8/9] repair(csv): preserve export boundary across dependency update --- .jules/sentinel.md | 4 ---- CHANGELOG.md | 1 + .../web/src/lib/server/csv/export.test.ts | 20 ++++++++++++++----- packages/web/src/lib/server/csv/export.ts | 11 +++++++++- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 7052482b..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. -## 2023-10-27 - CSV 매크로 인젝션 취약점 수정 -**Vulnerability:** 세션 데이터를 CSV로 내보낼 때 `csvField`에서 `=, +, -, @` 등으로 시작하는 값을 이스케이프하지 않아 스프레드시트 애플리케이션에서 수식/매크로로 실행될 수 있는 CSV Injection (스프레드시트 매크로 인젝션) 취약점이 발견되었습니다. -**Learning:** CSV 생성 시 단순히 쉼표와 따옴표만 이스케이프해서는 안 되며, 엑셀 등의 프로그램에서 수식으로 해석될 수 있는 특수문자로 시작하는 문자열을 반드시 처리해야 함을 배웠습니다. -**Prevention:** CSV 값을 생성하는 공통 유틸리티 함수(예: `csvField`)를 만들어 `=, +, -, @` 등의 위험 문자로 시작하는 문자열 앞에는 항상 싱글 쿼트(`'`)를 붙이도록 중앙에서 방어해야 합니다. diff --git a/CHANGELOG.md b/CHANGELOG.md index 19694452..99f2d37d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### 🛡️ 보안 (Security) +- 조직 세션 CSV 내보내기에서 문자열 셀이 공백·탭·개행 뒤에 `=`, `+`, `-`, `@` 또는 대응 전각 문자를 포함해 수식으로 해석될 수 있으면 앞에 `'`를 붙여 텍스트로 내보냅니다. 쉼표·따옴표·개행 CSV quoting과 숫자 타입의 기존 표현은 유지하며, 회귀 테스트로 위험 문자열과 정상 숫자 경계를 고정했습니다. - 로그인, 회원가입, 비밀번호 재설정이 하나의 공유 비밀번호 계약을 사용하도록 통합했습니다. 입력 처리량을 1,024자로 먼저 제한하고, 현재 `bcryptjs`가 완전하게 검증할 수 있는 72 UTF-8 바이트를 초과하는 값은 조용히 잘라내지 않고 거부합니다. ASCII와 다중 바이트 Unicode 경계 회귀 테스트 및 운영·표준 근거 문서를 함께 추가했습니다. ### ⚡ 성능 (Performance) diff --git a/packages/web/src/lib/server/csv/export.test.ts b/packages/web/src/lib/server/csv/export.test.ts index 0afff89e..6ec28895 100644 --- a/packages/web/src/lib/server/csv/export.test.ts +++ b/packages/web/src/lib/server/csv/export.test.ts @@ -14,7 +14,7 @@ describe('csvField', () => { expect(csvField('string\r\nwith\nnewlines')).toBe('"string\r\nwith\nnewlines"') }) - it('prevents CSV Injection by prepending a quote', () => { + it('neutralizes formula-leading string cells before CSV quoting', () => { expect(csvField('=1+1')).toBe(`'=1+1`) expect(csvField('+1')).toBe(`'+1`) expect(csvField('-1')).toBe(`'-1`) @@ -22,13 +22,23 @@ describe('csvField', () => { expect(csvField('\t=1+1')).toBe(`'\t=1+1`) expect(csvField('\r=1+1')).toBe(`"'\r=1+1"`) expect(csvField('\n=1+1')).toBe(`"'\n=1+1"`) - expect(csvField(' =1+1')).toBe(`' =1+1`) // space padding - expect(csvField('\uff1d1+1')).toBe(`'\uff1d1+1`) // full width equals + expect(csvField(' =1+1')).toBe(`' =1+1`) + expect(csvField('\u00a0=1+1')).toBe(`'\u00a0=1+1`) + expect(csvField('=SUM(1,2)')).toBe(`"'=SUM(1,2)"`) }) - it('preserves formatting of safe numbers', () => { + it('neutralizes full-width formula initiators used by some spreadsheet locales', () => { + expect(csvField('\uff1d1+1')).toBe(`'\uff1d1+1`) + expect(csvField('\uff0b1')).toBe(`'\uff0b1`) + expect(csvField('\uff0d1')).toBe(`'\uff0d1`) + expect(csvField('\uff20sum')).toBe(`'\uff20sum`) + }) + + it('preserves formatting of safe values', () => { + expect(csvField('')).toBe('') + expect(csvField("'already-text")).toBe("'already-text") expect(csvField(123)).toBe('123') - expect(csvField(-123)).toBe('-123') // numeric type is safe, not string + expect(csvField(-123)).toBe('-123') expect(csvField(0)).toBe('0') }) }) diff --git a/packages/web/src/lib/server/csv/export.ts b/packages/web/src/lib/server/csv/export.ts index e02b4603..47cf1aca 100644 --- a/packages/web/src/lib/server/csv/export.ts +++ b/packages/web/src/lib/server/csv/export.ts @@ -1,7 +1,16 @@ +/** + * Serialize one CSV cell while reducing spreadsheet formula-interpretation risk. + * + * String values whose first non-whitespace character is a known formula trigger + * receive a leading apostrophe before ordinary CSV quoting. Numeric values keep + * their numeric representation. This is an export-boundary mitigation, not a + * universal spreadsheet sandbox: downstream applications may reinterpret CSV + * content after edits or re-saving, so callers must not treat the output as a + * trusted executable document format. + */ export function csvField(value: string | number | null | undefined) { if (value === null || value === undefined) return '' - // Prevent CSV Injection (Macro Injection) if (typeof value === 'string') { const trimmed = value.trimStart() if (/^[=+\-@\t\r\n\uFF1D\uFF0B\uFF0D\uFF20]/.test(trimmed)) { From 82c1956ec703220d5ed28fd4acaa497918bc16e3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 14:15:17 +0900 Subject: [PATCH 9/9] repair(csv): separate dependency owner deltas --- package.json | 4 +--- pnpm-lock.yaml | 65 ++++++++++++++++++++++---------------------------- 2 files changed, 30 insertions(+), 39 deletions(-) diff --git a/package.json b/package.json index fccff836..d085ba62 100644 --- a/package.json +++ b/package.json @@ -34,9 +34,7 @@ "undici": "^7.29.0", "minimatch": "^10.0.0", "@hono/node-server": "^2.0.5", - "body-parser": "^2.3.0", - "browserslist": "4.28.9", - "deepmerge-ts": "8.0.2" + "body-parser": "^2.3.0" } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bfd9a9da..6dfd315f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,8 +22,6 @@ overrides: minimatch: ^10.0.0 '@hono/node-server': ^2.0.5 body-parser: ^2.3.0 - browserslist: 4.28.9 - deepmerge-ts: 8.0.2 pnpmfileChecksum: qsp27c6veblwg3gxusbbzrumtm @@ -1941,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 @@ -1964,8 +1962,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.9: - resolution: {integrity: sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==} + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -2008,9 +2006,6 @@ packages: caniuse-lite@1.0.30001793: resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} - caniuse-lite@1.0.30001810: - resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} - ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -2283,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==} @@ -2374,8 +2369,8 @@ packages: effect@3.21.0: resolution: {integrity: sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==} - electron-to-chromium@1.5.422: - resolution: {integrity: sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==} + electron-to-chromium@1.5.364: + resolution: {integrity: sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==} emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -3678,8 +3673,8 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - node-releases@2.0.54: - resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==} + node-releases@2.0.46: + resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} engines: {node: '>=18'} npm-run-path@4.0.1: @@ -4531,11 +4526,11 @@ packages: until-async@3.0.2: resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} - update-browserslist-db@1.3.2: - resolution: {integrity: sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: - browserslist: 4.28.9 + browserslist: '>= 4.21.0' uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -4850,7 +4845,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.7 '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.9 + browserslist: 4.28.2 lru-cache: 5.1.1 semver: 6.3.1 @@ -5664,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: @@ -6364,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: {} @@ -6394,13 +6389,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.9: + browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.11.21 - caniuse-lite: 1.0.30001810 - electron-to-chromium: 1.5.422 - node-releases: 2.0.54 - update-browserslist-db: 1.3.2(browserslist@4.28.9) + baseline-browser-mapping: 2.10.33 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.364 + node-releases: 2.0.46 + update-browserslist-db: 1.2.3(browserslist@4.28.2) bundle-name@4.1.0: dependencies: @@ -6448,8 +6443,6 @@ snapshots: caniuse-lite@1.0.30001793: {} - caniuse-lite@1.0.30001810: {} - ccount@2.0.1: {} chai@5.3.3: @@ -6667,7 +6660,7 @@ snapshots: deep-is@0.1.4: {} - deepmerge-ts@8.0.2: {} + deepmerge-ts@7.1.5: {} deepmerge@4.3.1: {} @@ -6747,7 +6740,7 @@ snapshots: '@standard-schema/spec': 1.1.0 fast-check: 3.23.2 - electron-to-chromium@1.5.422: {} + electron-to-chromium@1.5.364: {} emoji-regex@10.6.0: {} @@ -8413,7 +8406,7 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 - node-releases@2.0.54: {} + node-releases@2.0.46: {} npm-run-path@4.0.1: dependencies: @@ -9006,7 +8999,7 @@ snapshots: '@dotenvx/dotenvx': 1.70.0 '@modelcontextprotocol/sdk': 1.29.0(zod@3.25.76) '@types/validate-npm-package-name': 4.0.2 - browserslist: 4.28.9 + browserslist: 4.28.2 commander: 14.0.3 cosmiconfig: 9.0.1(typescript@5.9.3) dedent: 1.7.2 @@ -9498,9 +9491,9 @@ snapshots: until-async@3.0.2: {} - update-browserslist-db@1.3.2(browserslist@4.28.9): + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: - browserslist: 4.28.9 + browserslist: 4.28.2 escalade: 3.2.0 picocolors: 1.1.1