From a809a11df88168bcd4b5a983253f21ab837db63f Mon Sep 17 00:00:00 2001 From: Shayan SalehiRad <167648493+CodeinScrubs@users.noreply.github.com> Date: Thu, 13 Aug 2026 16:46:45 +0330 Subject: [PATCH] fix(security): keep Unicode generation offline --- CHANGELOG.md | 4 ++-- docs/SECURITY.md | 5 ++--- package.json | 1 - scripts/generate-bidi-data.ts | 28 +++++++--------------------- unicode/README.md | 9 ++++++--- 5 files changed, 17 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f78550..29eb377 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,8 @@ is published under the public `@bidilens` npm scope. preserving `$...$`, `$$...$$`, `$$$$`, and `\(...\)` recognition. - Expressed UTF-16-to-code-point range construction with bounded typed-array fills so input-derived offsets cannot be interpreted as object properties. -- Restricted downloaded Unicode data writes to the two repository-owned, - version-pinned destination paths after exact SHA-256 verification. +- Removed the generator's network-to-filesystem refresh mode; Unicode table + generation now reads only two vendored, version- and SHA-256-pinned files. ### Direction correctness diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 3ad3a42..fa8f01f 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -44,9 +44,8 @@ pops/openers, formatting that crosses an isolate boundary, and U+200B. - recursive CLI scanning skips symbolic links. - raw-text math delimiter discovery is a single forward scan; repeated unmatched delimiters cannot trigger polynomial regular-expression work. -- the optional Unicode download command accepts only two source/destination - pairs pinned in repository code and verifies each exact SHA-256 before a - write; untrusted text never reaches that build-time path. +- Unicode generation is offline-only: it reads two vendored, SHA-256-pinned + source files and exposes no network-to-filesystem refresh path. ## False-positive policy diff --git a/package.json b/package.json index f50f7c3..2fc0b77 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "build:visual-fixtures": "pnpm --filter @bidilens/core run build && pnpm --filter @bidilens/markdown run build && pnpm --filter @bidilens/react run build && pnpm --filter @bidilens/web-component run build", "test:visual": "pnpm run build:visual-fixtures && playwright test", "unicode:generate": "tsx scripts/generate-bidi-data.ts", - "unicode:download": "tsx scripts/generate-bidi-data.ts --download", "unicode:check": "tsx scripts/generate-bidi-data.ts --check", "release:check": "tsx scripts/release-check.ts", "android:check": "tsx scripts/run-android.ts", diff --git a/scripts/generate-bidi-data.ts b/scripts/generate-bidi-data.ts index 87d864b..0f5c088 100644 --- a/scripts/generate-bidi-data.ts +++ b/scripts/generate-bidi-data.ts @@ -40,8 +40,6 @@ const UNICODE_SOURCES = [ } ] as const satisfies readonly UnicodeSource[]; -const PINNED_UNICODE_PATHS = new Set(UNICODE_SOURCES.map((source) => source.path)); - const CLASS = { L: 0, R: 1, @@ -270,36 +268,24 @@ ${renderRustRanges('NATURAL_LETTER_RANGES', ranges.naturalLetters)} `; } -async function sourceBytes(source: UnicodeSource, download: boolean): Promise { - let bytes: Uint8Array; - if (download) { - const response = await fetch(source.url); - if (!response.ok) throw new Error(`${source.label} download failed: HTTP ${response.status}`); - bytes = new Uint8Array(await response.arrayBuffer()); - } else { - bytes = await readFile(source.path); - } +function verifiedBytes(source: UnicodeSource, bytes: Uint8Array): Uint8Array { const actualSha256 = createHash('sha256').update(bytes).digest('hex'); if (actualSha256 !== source.sha256) { throw new Error(`${source.label} checksum mismatch: expected ${source.sha256}, received ${actualSha256}`); } - if (download) { - if (!PINNED_UNICODE_PATHS.has(source.path)) { - throw new Error(`Refusing to write outside the pinned Unicode source paths: ${source.path}`); - } - await mkdir(dirname(source.path), { recursive: true }); - await writeFile(source.path, bytes); - } return bytes; } +async function localSourceBytes(source: UnicodeSource): Promise { + return verifiedBytes(source, await readFile(source.path)); +} + async function main(): Promise { - const download = process.argv.includes('--download'); const check = process.argv.includes('--check'); const [bidiSource, generalCategorySource] = UNICODE_SOURCES; const [bidiBytes, generalCategoryBytes] = await Promise.all([ - sourceBytes(bidiSource, download), - sourceBytes(generalCategorySource, download) + localSourceBytes(bidiSource), + localSourceBytes(generalCategorySource) ]); const ranges = collectGeneratedRanges( new TextDecoder().decode(bidiBytes), diff --git a/unicode/README.md b/unicode/README.md index 411dc12..f0e7823 100644 --- a/unicode/README.md +++ b/unicode/README.md @@ -21,11 +21,14 @@ To reproduce the generated table from the vendored source: pnpm unicode:generate ``` -To deliberately refresh the pinned upstream file, first update the version, -URLs, and both expected SHA-256 values in `scripts/generate-bidi-data.ts`, then run: +To deliberately refresh the pinned upstream files, download both exact source +URLs shown in `scripts/generate-bidi-data.ts` outside this repository, verify +their expected SHA-256 values, replace only the two vendored files, then update +the version and hashes in the generator. The generator deliberately has no +network-to-filesystem mode. Review with: ```bash -pnpm unicode:download +pnpm unicode:generate git diff -- unicode packages/core/src/generated/bidi-ranges.ts pnpm run check ```