From fa26ee93edf0e1bf7fe9fd445fb50169a45d0c38 Mon Sep 17 00:00:00 2001 From: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Date: Mon, 17 Aug 2026 20:23:24 -0300 Subject: [PATCH] fix(security): clear dependency and ReDoS alerts --- package.json | 8 - pnpm-lock.yaml | 349 ++++++++++++++++++++--------------- pnpm-workspace.yaml | 12 ++ src/core/factsheet.ts | 2 +- src/core/history.ts | 2 +- src/core/secret-guard.ts | 230 +++++++++++++++++------ tests/security-redos.test.ts | 26 +++ 7 files changed, 409 insertions(+), 220 deletions(-) create mode 100644 tests/security-redos.test.ts diff --git a/package.json b/package.json index 608bcf0..8950f5d 100644 --- a/package.json +++ b/package.json @@ -91,14 +91,6 @@ "vitest": "^4.1.6", "wrangler": "^4.92.0" }, - "pnpm": { - "overrides": { - "ws@>=8.0.0 <8.20.1": "^8.20.1", - "vite@>=8.0.0 <8.0.16": "^8.0.16", - "undici@>=7.0.0 <7.28.0": "^7.28.0", - "esbuild@>=0.27.3 <0.28.1": "^0.28.1" - } - }, "dependencies": { "gpt-tokenizer": "^3.4.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 618a85b..58c8136 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,10 +5,14 @@ settings: excludeLinksFromLockfile: false overrides: - ws@>=8.0.0 <8.20.1: ^8.20.1 - vite@>=8.0.0 <8.0.16: ^8.0.16 - undici@>=7.0.0 <7.28.0: ^7.28.0 + brace-expansion@>=4.0.0 <5.0.9: 5.0.9 esbuild@>=0.27.3 <0.28.1: ^0.28.1 + nanoid@<3.3.18: 3.3.18 + postcss@<=8.5.22: 8.5.23 + sharp@<0.35.0: 0.35.3 + undici@>=7.0.0 <7.29.0: 7.29.0 + vite@>=8.0.0 <8.0.16: ^8.0.16 + ws@>=8.0.0 <8.20.1: ^8.20.1 importers: @@ -53,7 +57,7 @@ importers: version: 4.1.6(@types/node@20.19.41)(vite@8.0.16(@types/node@20.19.41)(esbuild@0.28.2)(tsx@4.22.2)) wrangler: specifier: ^4.92.0 - version: 4.92.0(@cloudflare/workers-types@4.20260518.1) + version: 4.92.0(@cloudflare/workers-types@4.20260518.1)(@types/node@20.19.41) packages: @@ -113,6 +117,9 @@ packages: '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} @@ -335,152 +342,161 @@ packages: resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.34.5': - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-darwin-arm64@0.35.3': + resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.34.5': - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-darwin-x64@0.35.3': + resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + '@img/sharp-freebsd-wasm32@0.35.3': + resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.2': + resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + '@img/sharp-libvips-darwin-x64@1.3.2': + resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + '@img/sharp-libvips-linux-arm64@1.3.2': + resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm@1.2.4': - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + '@img/sharp-libvips-linux-arm@1.3.2': + resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + '@img/sharp-libvips-linux-ppc64@1.3.2': + resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + '@img/sharp-libvips-linux-riscv64@1.3.2': + resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + '@img/sharp-libvips-linux-s390x@1.3.2': + resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-x64@1.2.4': - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + '@img/sharp-libvips-linux-x64@1.3.2': + resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + '@img/sharp-libvips-linuxmusl-x64@1.3.2': + resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-linux-arm64@0.34.5': - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-arm64@0.35.3': + resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-linux-arm@0.34.5': - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-arm@0.35.3': + resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==} + engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-linux-ppc64@0.34.5': - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-ppc64@0.35.3': + resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==} + engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-linux-riscv64@0.34.5': - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-riscv64@0.35.3': + resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==} + engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-linux-s390x@0.34.5': - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-s390x@0.35.3': + resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==} + engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-linux-x64@0.34.5': - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-x64@0.35.3': + resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linuxmusl-arm64@0.35.3': + resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-linuxmusl-x64@0.34.5': - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linuxmusl-x64@0.35.3': + resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-wasm32@0.34.5': - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-wasm32@0.35.3': + resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.3': + resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==} + engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.34.5': - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-arm64@0.35.3': + resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.34.5': - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-ia32@0.35.3': + resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==} + engines: {node: ^20.9.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.34.5': - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-x64@0.35.3': + resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] @@ -828,9 +844,9 @@ packages: blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} - brace-expansion@5.0.7: - resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} - engines: {node: 18 || 20 || >=22} + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} @@ -1108,8 +1124,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.12: - resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -1152,8 +1168,8 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - postcss@8.5.15: - resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -1174,9 +1190,19 @@ packages: engines: {node: '>=10'} hasBin: true - sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.35.3: + resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} @@ -1255,8 +1281,8 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici@7.28.0: - resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} + undici@7.29.0: + resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} engines: {node: '>=20.18.1'} unenv@2.0.0-rc.24: @@ -1442,6 +1468,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 @@ -1577,98 +1608,108 @@ snapshots: '@img/colour@1.1.0': {} - '@img/sharp-darwin-arm64@0.34.5': + '@img/sharp-darwin-arm64@0.35.3': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-arm64': 1.3.2 optional: true - '@img/sharp-darwin-x64@0.34.5': + '@img/sharp-darwin-x64@0.35.3': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.3.2 optional: true - '@img/sharp-libvips-darwin-arm64@1.2.4': + '@img/sharp-freebsd-wasm32@0.35.3': + dependencies: + '@img/sharp-wasm32': 0.35.3 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.2': optional: true - '@img/sharp-libvips-darwin-x64@1.2.4': + '@img/sharp-libvips-darwin-x64@1.3.2': optional: true - '@img/sharp-libvips-linux-arm64@1.2.4': + '@img/sharp-libvips-linux-arm64@1.3.2': optional: true - '@img/sharp-libvips-linux-arm@1.2.4': + '@img/sharp-libvips-linux-arm@1.3.2': optional: true - '@img/sharp-libvips-linux-ppc64@1.2.4': + '@img/sharp-libvips-linux-ppc64@1.3.2': optional: true - '@img/sharp-libvips-linux-riscv64@1.2.4': + '@img/sharp-libvips-linux-riscv64@1.3.2': optional: true - '@img/sharp-libvips-linux-s390x@1.2.4': + '@img/sharp-libvips-linux-s390x@1.3.2': optional: true - '@img/sharp-libvips-linux-x64@1.2.4': + '@img/sharp-libvips-linux-x64@1.3.2': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + '@img/sharp-libvips-linuxmusl-arm64@1.3.2': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.4': + '@img/sharp-libvips-linuxmusl-x64@1.3.2': optional: true - '@img/sharp-linux-arm64@0.34.5': + '@img/sharp-linux-arm64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.3.2 optional: true - '@img/sharp-linux-arm@0.34.5': + '@img/sharp-linux-arm@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.3.2 optional: true - '@img/sharp-linux-ppc64@0.34.5': + '@img/sharp-linux-ppc64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.3.2 optional: true - '@img/sharp-linux-riscv64@0.34.5': + '@img/sharp-linux-riscv64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.3.2 optional: true - '@img/sharp-linux-s390x@0.34.5': + '@img/sharp-linux-s390x@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.3.2 optional: true - '@img/sharp-linux-x64@0.34.5': + '@img/sharp-linux-x64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.3.2 optional: true - '@img/sharp-linuxmusl-arm64@0.34.5': + '@img/sharp-linuxmusl-arm64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 optional: true - '@img/sharp-linuxmusl-x64@0.34.5': + '@img/sharp-linuxmusl-x64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 optional: true - '@img/sharp-wasm32@0.34.5': + '@img/sharp-wasm32@0.35.3': dependencies: - '@emnapi/runtime': 1.10.0 + '@emnapi/runtime': 1.11.3 optional: true - '@img/sharp-win32-arm64@0.34.5': + '@img/sharp-webcontainers-wasm32@0.35.3': + dependencies: + '@img/sharp-wasm32': 0.35.3 + optional: true + + '@img/sharp-win32-arm64@0.35.3': optional: true - '@img/sharp-win32-ia32@0.34.5': + '@img/sharp-win32-ia32@0.35.3': optional: true - '@img/sharp-win32-x64@0.34.5': + '@img/sharp-win32-x64@0.35.3': optional: true '@jridgewell/resolve-uri@3.1.2': {} @@ -1978,7 +2019,7 @@ snapshots: blake3-wasm@2.1.5: {} - brace-expansion@5.0.7: + brace-expansion@5.0.9: dependencies: balanced-match: 4.0.4 @@ -2230,25 +2271,26 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - miniflare@4.20260515.0: + miniflare@4.20260515.0(@types/node@20.19.41): dependencies: '@cspotcode/source-map-support': 0.8.1 - sharp: 0.34.5 - undici: 7.28.0 + sharp: 0.35.3(@types/node@20.19.41) + undici: 7.29.0 workerd: 1.20260515.1 ws: 8.21.0 youch: 4.1.0-beta.10 transitivePeerDependencies: + - '@types/node' - bufferutil - utf-8-validate minimatch@10.2.5: dependencies: - brace-expansion: 5.0.7 + brace-expansion: 5.0.9 ms@2.1.3: {} - nanoid@3.3.12: {} + nanoid@3.3.18: {} natural-compare@1.4.0: {} @@ -2283,9 +2325,9 @@ snapshots: picomatch@4.0.4: {} - postcss@8.5.15: + postcss@8.5.23: dependencies: - nanoid: 3.3.12 + nanoid: 3.3.18 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -2316,36 +2358,40 @@ snapshots: semver@7.8.0: {} - sharp@0.34.5: + semver@7.8.5: {} + + sharp@0.35.3(@types/node@20.19.41): dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 - semver: 7.8.0 + semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 + '@img/sharp-darwin-arm64': 0.35.3 + '@img/sharp-darwin-x64': 0.35.3 + '@img/sharp-freebsd-wasm32': 0.35.3 + '@img/sharp-libvips-darwin-arm64': 1.3.2 + '@img/sharp-libvips-darwin-x64': 1.3.2 + '@img/sharp-libvips-linux-arm': 1.3.2 + '@img/sharp-libvips-linux-arm64': 1.3.2 + '@img/sharp-libvips-linux-ppc64': 1.3.2 + '@img/sharp-libvips-linux-riscv64': 1.3.2 + '@img/sharp-libvips-linux-s390x': 1.3.2 + '@img/sharp-libvips-linux-x64': 1.3.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + '@img/sharp-linux-arm': 0.35.3 + '@img/sharp-linux-arm64': 0.35.3 + '@img/sharp-linux-ppc64': 0.35.3 + '@img/sharp-linux-riscv64': 0.35.3 + '@img/sharp-linux-s390x': 0.35.3 + '@img/sharp-linux-x64': 0.35.3 + '@img/sharp-linuxmusl-arm64': 0.35.3 + '@img/sharp-linuxmusl-x64': 0.35.3 + '@img/sharp-webcontainers-wasm32': 0.35.3 + '@img/sharp-win32-arm64': 0.35.3 + '@img/sharp-win32-ia32': 0.35.3 + '@img/sharp-win32-x64': 0.35.3 + '@types/node': 20.19.41 shebang-command@2.0.0: dependencies: @@ -2411,7 +2457,7 @@ snapshots: undici-types@6.21.0: {} - undici@7.28.0: {} + undici@7.29.0: {} unenv@2.0.0-rc.24: dependencies: @@ -2425,7 +2471,7 @@ snapshots: dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.15 + postcss: 8.5.23 rolldown: 1.0.3 tinyglobby: 0.2.17 optionalDependencies: @@ -2480,13 +2526,13 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260515.1 '@cloudflare/workerd-windows-64': 1.20260515.1 - wrangler@4.92.0(@cloudflare/workers-types@4.20260518.1): + wrangler@4.92.0(@cloudflare/workers-types@4.20260518.1)(@types/node@20.19.41): dependencies: '@cloudflare/kv-asset-handler': 0.5.0 '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260515.1) blake3-wasm: 2.1.5 esbuild: 0.28.2 - miniflare: 4.20260515.0 + miniflare: 4.20260515.0(@types/node@20.19.41) path-to-regexp: 6.3.0 unenv: 2.0.0-rc.24 workerd: 1.20260515.1 @@ -2494,6 +2540,7 @@ snapshots: '@cloudflare/workers-types': 4.20260518.1 fsevents: 2.3.3 transitivePeerDependencies: + - '@types/node' - bufferutil - utf-8-validate diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 54dd355..17aa960 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,6 +3,18 @@ onlyBuiltDependencies: - sharp - workerd +# pnpm 10 reads workspace-wide overrides from this file (not package.json). +# Keep vulnerable transitive ranges above their advisory fixed versions. +overrides: + "brace-expansion@>=4.0.0 <5.0.9": "5.0.9" + "esbuild@>=0.27.3 <0.28.1": "^0.28.1" + "nanoid@<3.3.18": "3.3.18" + "postcss@<=8.5.22": "8.5.23" + "sharp@<0.35.0": "0.35.3" + "undici@>=7.0.0 <7.29.0": "7.29.0" + "vite@>=8.0.0 <8.0.16": "^8.0.16" + "ws@>=8.0.0 <8.20.1": "^8.20.1" + # Supply-chain hardening: refuse to install any package version published # less than 3 days ago. Catches the typical malicious-package lifecycle # (publish → discovered → unpublished within hours/days). Legitimate diff --git a/src/core/factsheet.ts b/src/core/factsheet.ts index 83be1a8..797ec67 100644 --- a/src/core/factsheet.ts +++ b/src/core/factsheet.ts @@ -317,5 +317,5 @@ export function appendIdsBlock(text: string, maxIds = 16): string { } if (tokens.length === 0) return text; const lines = tokens.map((t) => `${idsBlockLabel(t)} ${t}`); - return `${text.replace(/\s+$/, '')}\nIDS\n${lines.join('\n')}\n`; + return `${text.trimEnd()}\nIDS\n${lines.join('\n')}\n`; } diff --git a/src/core/history.ts b/src/core/history.ts index 997ffaf..259f9ae 100644 --- a/src/core/history.ts +++ b/src/core/history.ts @@ -698,7 +698,7 @@ export async function collapseHistory( if (chunkSlot !== undefined) { const idsAt = withIds.lastIndexOf('\nIDS\n'); const suffix = idsAt >= 0 ? withIds.slice(idsAt) : ''; - chunkSlot = chunkSlot.replace(/\s+$/, '') + suffix; + chunkSlot = chunkSlot.trimEnd() + suffix; } chunkRender = withIds; } diff --git a/src/core/secret-guard.ts b/src/core/secret-guard.ts index c18359e..6d50256 100644 --- a/src/core/secret-guard.ts +++ b/src/core/secret-guard.ts @@ -23,7 +23,10 @@ const KEY_PATTERNS: readonly RegExp[] = [ /\bAKIA[0-9A-Z]{16}\b/g, // AWS access key id /\bAIza[0-9A-Za-z_-]{35}\b/g, // Google API key ]; -const PEM_PATTERN = /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g; +const PEM_BEGIN_PREFIX = '-----BEGIN '; +const PEM_END_PREFIX = '-----END '; +const PEM_PRIVATE_SUFFIX = 'PRIVATE KEY-----'; +const PEM_LABEL_MAX = 64; // The VALUE class is an explicit allowlist that already excludes U+21B5 (↵, // the reflow newline sentinel — see render.ts NL_SENTINEL), so a Bearer token // abutting a reflowed line break can't swallow it. Unlike ASSIGNMENT below, @@ -35,8 +38,17 @@ const BEARER_PATTERN = /\bBearer\s+([A-Za-z0-9._~+/=-]{20,})/g; // and let the captured VALUE bleed across a line boundary into the next // line's text whenever the secret's value directly abuts a ↵ (the common // case — reflow joins lines with no separating whitespace). -const ASSIGNMENT_PATTERN = - /\b[A-Z0-9_]*(?:API|SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE|CREDENTIAL|ACCESS)[A-Z0-9_]*\s*[=:]\s*([^\s↵]{8,})/g; +const ASSIGNMENT_PATTERN = /\b([A-Z0-9_]+)[ \t]*[=:][ \t]*([^\s↵]{8,})/g; +const SECRET_ASSIGNMENT_FRAGMENTS = [ + 'API', + 'SECRET', + 'TOKEN', + 'PASSWORD', + 'PASSWD', + 'PRIVATE', + 'CREDENTIAL', + 'ACCESS', +] as const; // Public high-entropy shapes the codebase already trusts (factsheet.ts grammar). // Kept as local copies: factsheet.ts does not export them, and the two modules @@ -94,66 +106,134 @@ function isolateAssignmentValue(chunk: string): { value: string; offset: number return { value: chunk, offset: 0 }; } -/** All secret spans in `text`, sorted by start. Overlapping hits collapse to - * the more specific kind first (pem/key/bearer/assignment over entropy), - * then earliest-start/longest-match — never the first pattern that happened - * to run. This matters because the entropy fallback can produce a hit that - * starts earlier than, and fully contains, a narrower pem/key/bearer/ - * assignment hit: a whole `NAME=value` chunk can itself read as high-entropy - * (e.g. the raw text still contains the low-entropy `NAME=` prefix inside a - * wider `\S+` token boundary), while the assignment pattern only claims the - * value's span. Picking "earliest, then longest" alone would keep the vague - * entropy hit and lose the precise kind; specificity-first keeps the - * precise one so redaction never splices twice. */ -export function findSecrets(text: string): SecretHit[] { - if (!text) return []; - const hits: SecretHit[] = []; - const push = (start: number, end: number, kind: string) => hits.push({ start, end, kind }); +function isPemLabel(label: string): boolean { + if (label.length > PEM_LABEL_MAX) return false; + for (const code of label) { + if (code !== ' ' && (code < 'A' || code > 'Z')) return false; + } + return true; +} + +function findPemMarker( + line: string, + prefix: string, + from: number, +): { start: number; end: number } | undefined { + let markerStart = line.indexOf(prefix, from); + while (markerStart >= 0) { + const labelStart = markerStart + prefix.length; + // Inspect a bounded header window so repeated fake BEGIN/END prefixes can + // never make suffix search re-scan the rest of an attacker-controlled line. + const window = line.slice( + labelStart, + labelStart + PEM_LABEL_MAX + PEM_PRIVATE_SUFFIX.length, + ); + const suffixOffset = window.indexOf(PEM_PRIVATE_SUFFIX); + if (suffixOffset >= 0) { + const label = window.slice(0, suffixOffset); + if (isPemLabel(label)) { + return { + start: markerStart, + end: labelStart + suffixOffset + PEM_PRIVATE_SUFFIX.length, + }; + } + } + markerStart = line.indexOf(prefix, labelStart); + } + return undefined; +} - for (const m of text.matchAll(PEM_PATTERN)) push(m.index, m.index + m[0].length, 'pem'); +/** Locate complete PEM private-key blocks with one forward pass over the text. + * PEM markers are line-oriented, so no regex needs to backtrack across an + * attacker-controlled body. The end label intentionally need not match the + * begin label, preserving the previous guard's fail-safe detection behavior. */ +function scanPemLine( + line: string, + lineStart: number, + openStart: number | undefined, + spans: Array<{ start: number; end: number }>, +): number | undefined { + let cursor = 0; + while (cursor <= line.length) { + const prefix = openStart === undefined ? PEM_BEGIN_PREFIX : PEM_END_PREFIX; + const marker = findPemMarker(line, prefix, cursor); + if (!marker) return openStart; + if (openStart === undefined) { + openStart = lineStart + marker.start; + } else { + spans.push({ start: openStart, end: lineStart + marker.end }); + openStart = undefined; + } + cursor = marker.end; + } + return openStart; +} + +function findPemSpans(text: string): Array<{ start: number; end: number }> { + const spans: Array<{ start: number; end: number }> = []; + let openStart: number | undefined; + let lineStart = 0; + for (const line of text.split('\n')) { + openStart = scanPemLine(line, lineStart, openStart, spans); + lineStart += line.length + 1; + } + return spans; +} + +function findKeyHits(text: string): SecretHit[] { + const hits: SecretHit[] = []; for (const re of KEY_PATTERNS) { - for (const m of text.matchAll(re)) push(m.index, m.index + m[0].length, 'key'); + for (const m of text.matchAll(re)) hits.push({ start: m.index, end: m.index + m[0].length, kind: 'key' }); } + return hits; +} + +function findBearerHits(text: string): SecretHit[] { + const hits: SecretHit[] = []; for (const m of text.matchAll(BEARER_PATTERN)) { - const tok = m[1]!; - const start = m.index + m[0].indexOf(tok); - if (!isPublicChunk(tok)) push(start, start + tok.length, 'bearer'); + const token = m[1]!; + if (isPublicChunk(token)) continue; + const start = m.index + m[0].indexOf(token); + hits.push({ start, end: start + token.length, kind: 'bearer' }); } + return hits; +} + +function findAssignmentHits(text: string): SecretHit[] { + const hits: SecretHit[] = []; for (const m of text.matchAll(ASSIGNMENT_PATTERN)) { - const val = m[1]!; - if (val.includes('[REDACTED:')) continue; // idempotency under re-runs - const start = m.index + m[0].lastIndexOf(val); - push(start, start + val.length, 'assignment'); + const name = m[1]!; + const value = m[2]!; + if (!SECRET_ASSIGNMENT_FRAGMENTS.some((fragment) => name.includes(fragment))) continue; + if (value.includes('[REDACTED:')) continue; + const start = m.index + m[0].lastIndexOf(value); + hits.push({ start, end: start + value.length, kind: 'assignment' }); } - // Entropy fallback over whitespace-free chunks. A NAME=value chunk is - // judged on its value alone (see isolateAssignmentValue) so a secret value - // behind an innocuous name is still caught. - // - // ↵ (U+21B5) is the history reflow's hard-newline sentinel (render.ts - // NL_SENTINEL) — reflow() joins lines with NO surrounding whitespace, so a - // plain `\S+` chunker treats ↵ as ordinary content and glues an entire - // multi-line, secret-free transcript into ONE chunk spanning every line, - // which reads as high-entropy and false-positives as a secret. ↵ is a - // token BOUNDARY (it marks where a line ends), never token content, so it - // must split chunks exactly like whitespace does. + return hits; +} + +function findEntropyHits(text: string): SecretHit[] { + const hits: SecretHit[] = []; for (const m of text.matchAll(/[^\s↵]+/g)) { const chunk = m[0]; if (chunk.includes('[REDACTED:')) continue; const { value, offset } = isolateAssignmentValue(chunk); if (value.length < ENTROPY_MIN_LEN || value.length > ENTROPY_MAX_LEN) continue; - if (isPublicChunk(value)) continue; - if (shannonBitsPerChar(value) < ENTROPY_BITS) continue; - push(m.index + offset, m.index + offset + value.length, 'entropy'); + if (isPublicChunk(value) || shannonBitsPerChar(value) < ENTROPY_BITS) continue; + hits.push({ start: m.index + offset, end: m.index + offset + value.length, kind: 'entropy' }); } + return hits; +} - const SPECIFICITY: Record = { - pem: 5, - key: 4, - bearer: 3, - assignment: 2, - entropy: 1, - }; +const SPECIFICITY: Record = { + pem: 5, + key: 4, + bearer: 3, + assignment: 2, + entropy: 1, +}; +function collapseOverlappingHits(hits: SecretHit[]): SecretHit[] { hits.sort((a, b) => { if (a.start !== b.start) return a.start - b.start; if (a.end !== b.end) return b.end - a.end; @@ -162,22 +242,54 @@ export function findSecrets(text: string): SecretHit[] { const out: SecretHit[] = []; let lastEnd = -1; - for (const h of hits) { - if (h.start < lastEnd) { - const prevHit = out[out.length - 1]; - if ((SPECIFICITY[prevHit!.kind] ?? 0) >= (SPECIFICITY[h.kind] ?? 0)) { - continue; - } - out[out.length - 1] = h; - lastEnd = h.end; - } else { - out.push(h); - lastEnd = h.end; + for (const hit of hits) { + if (hit.start >= lastEnd) { + out.push(hit); + lastEnd = hit.end; + continue; } + const previous = out[out.length - 1]!; + if ((SPECIFICITY[previous.kind] ?? 0) >= (SPECIFICITY[hit.kind] ?? 0)) continue; + out[out.length - 1] = hit; + lastEnd = hit.end; } return out; } +/** All secret spans in `text`, sorted by start. Overlapping hits collapse to + * the more specific kind first (pem/key/bearer/assignment over entropy), + * then earliest-start/longest-match — never the first pattern that happened + * to run. This matters because the entropy fallback can produce a hit that + * starts earlier than, and fully contains, a narrower pem/key/bearer/ + * assignment hit: a whole `NAME=value` chunk can itself read as high-entropy + * (e.g. the raw text still contains the low-entropy `NAME=` prefix inside a + * wider `\S+` token boundary), while the assignment pattern only claims the + * value's span. Picking "earliest, then longest" alone would keep the vague + * entropy hit and lose the precise kind; specificity-first keeps the + * precise one so redaction never splices twice. */ +export function findSecrets(text: string): SecretHit[] { + if (!text) return []; + // Entropy fallback over whitespace-free chunks. A NAME=value chunk is + // judged on its value alone (see isolateAssignmentValue) so a secret value + // behind an innocuous name is still caught. + // + // ↵ (U+21B5) is the history reflow's hard-newline sentinel (render.ts + // NL_SENTINEL) — reflow() joins lines with NO surrounding whitespace, so a + // plain `\S+` chunker treats ↵ as ordinary content and glues an entire + // multi-line, secret-free transcript into ONE chunk spanning every line, + // which reads as high-entropy and false-positives as a secret. ↵ is a + // token BOUNDARY (it marks where a line ends), never token content, so it + // must split chunks exactly like whitespace does. + const pemHits = findPemSpans(text).map(({ start, end }) => ({ start, end, kind: 'pem' })); + return collapseOverlappingHits([ + ...pemHits, + ...findKeyHits(text), + ...findBearerHits(text), + ...findAssignmentHits(text), + ...findEntropyHits(text), + ]); +} + /** Prefix-preserving mask: keeps 4 chars for debuggability, kills the secret. * Deterministic and idempotent (masked text produces zero new hits). */ export function redactSecrets(text: string): { text: string; hits: number } { diff --git a/tests/security-redos.test.ts b/tests/security-redos.test.ts new file mode 100644 index 0000000..7641d7c --- /dev/null +++ b/tests/security-redos.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; + +import { appendIdsBlock } from '../src/core/factsheet.js'; +import { findSecrets } from '../src/core/secret-guard.js'; + +describe('security — adversarial text remains bounded', () => { + it('rejects repeated PEM and assignment prefixes without polynomial backtracking', () => { + const pemNoise = '-----BEGIN PRIVATE KEY-----'.repeat(3_000); + const assignmentNoise = `${'API'.repeat(30_000)} not-an-assignment`; + const started = performance.now(); + + expect(findSecrets(`${pemNoise}\n${assignmentNoise}`)).toEqual([]); + + // A linear scan of ~170 KiB stays comfortably below this on the slowest + // supported CI runtime. The vulnerable regexes took multiple seconds. + expect(performance.now() - started).toBeLessThan(1_000); + }, 10_000); + + it('appends precision IDs without rescanning a long whitespace prefix', () => { + const started = performance.now(); + const result = appendIdsBlock(`${' '.repeat(50_000)}commit 9d121ac`); + + expect(result).toContain('\nIDS\nhex 9d121ac\n'); + expect(performance.now() - started).toBeLessThan(1_000); + }, 10_000); +});