From a1c7466de85cb791d1548d5eb738d6d1b4e6f3f7 Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Sun, 28 Apr 2024 15:22:03 +0000 Subject: [PATCH] Ensure that source files only contain ASCII This change should have no user impact. This is a defense-in-depth against someone opening a malicious patch that has non-ASCII characters. --- package-lock.json | 35 +++++++++++---- package.json | 1 + test/content-security-policy.test.ts | 4 +- test/source-files.test.ts | 64 ++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 10 deletions(-) create mode 100644 test/source-files.test.ts diff --git a/package-lock.json b/package-lock.json index f82d8c2..0f5f508 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "@rollup/plugin-typescript": "^11.1.6", "@types/connect": "^3.4.38", "@types/jest": "^29.5.12", + "@types/node": "^20.12.7", "@types/node-zopfli": "^2.0.5", "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^7.7.1", @@ -2030,10 +2031,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "14.14.16", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.16.tgz", - "integrity": "sha512-naXYePhweTi+BMv11TgioE2/FXU4fSl29HAH1ffxVciNsH3rYXjNP2yM8wqmSm7jS20gM8TIklKiTen+1iVncw==", - "dev": true + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/node-zopfli": { "version": "2.0.5", @@ -6515,6 +6519,12 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -8124,10 +8134,13 @@ "dev": true }, "@types/node": { - "version": "14.14.16", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.16.tgz", - "integrity": "sha512-naXYePhweTi+BMv11TgioE2/FXU4fSl29HAH1ffxVciNsH3rYXjNP2yM8wqmSm7jS20gM8TIklKiTen+1iVncw==", - "dev": true + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } }, "@types/node-zopfli": { "version": "2.0.5", @@ -11405,6 +11418,12 @@ "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index 05fed56..68e1116 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "@rollup/plugin-typescript": "^11.1.6", "@types/connect": "^3.4.38", "@types/jest": "^29.5.12", + "@types/node": "^20.12.7", "@types/node-zopfli": "^2.0.5", "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^7.7.1", diff --git a/test/content-security-policy.test.ts b/test/content-security-policy.test.ts index db07587..00dfe6f 100644 --- a/test/content-security-policy.test.ts +++ b/test/content-security-policy.test.ts @@ -297,12 +297,12 @@ describe("Content-Security-Policy middleware", () => { const invalidNames = [ "", ";", - "á", + "\u00e1", "default src", "default;src", "default,src", "default!src", - "defáult-src", + "def\u00e1ult-src", "default_src", "__proto__", ]; diff --git a/test/source-files.test.ts b/test/source-files.test.ts new file mode 100644 index 0000000..f6a2a81 --- /dev/null +++ b/test/source-files.test.ts @@ -0,0 +1,64 @@ +import { promisify } from "node:util"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import * as childProcess from "node:child_process"; +import { ReadableStream } from "node:stream/web"; + +const EXTNAMES_THAT_DONT_HAVE_TO_BE_ASCII: ReadonlySet = new Set([ + ".md", +]); +const NEWLINE = "\n".charCodeAt(0); +const SPACE = " ".charCodeAt(0); +const TILDE = "~".charCodeAt(0); + +const exec = promisify(childProcess.exec); +const root = path.resolve(__dirname, ".."); + +describe("source files", () => { + it('only has "normal" ASCII characters in the source files', async () => { + for await (const { sourceFile, chunk, index } of getSourceFileChunks()) { + const abnormalByte = chunk.find((byte) => !isNormalAsciiByte(byte)); + if (typeof abnormalByte === "number") { + throw new Error( + `${sourceFile} must only contain "normal" ASCII characters but contained 0x${abnormalByte.toString(16)} at index ${index + chunk.indexOf(abnormalByte)}`, + ); + } + } + }); +}); + +const getSourceFileChunks = (): AsyncIterable<{ + sourceFile: string; + chunk: Uint8Array; + index: number; +}> => + new ReadableStream({ + async start(controller) { + await Promise.all( + (await getSourceFiles()).map(async (sourceFile) => { + const handle = await fs.open(sourceFile); + let index = 0; + for await (const chunk of handle.readableWebStream({ + type: "bytes", + })) { + controller.enqueue({ sourceFile, chunk, index }); + index += chunk.byteLength; + } + await handle.close(); + }), + ); + controller.close(); + }, + }); + +const getSourceFiles = async (): Promise> => + (await exec("git ls-files", { cwd: root, env: {} })).stdout + .split(/\r?\n/g) + .filter( + (file) => !EXTNAMES_THAT_DONT_HAVE_TO_BE_ASCII.has(path.extname(file)), + ) + .filter(Boolean) + .map((line) => path.resolve(root, line)); + +const isNormalAsciiByte = (byte: number): boolean => + byte === NEWLINE || (byte >= SPACE && byte <= TILDE);