From d568b8dfef8316e5adb4cfcb06b560dbcf6c2fab Mon Sep 17 00:00:00 2001 From: Eric Fornaciari Date: Fri, 20 Feb 2026 09:30:21 -0800 Subject: [PATCH 1/4] fix(security): resolve CodeQL code alerts Resolves CodeQL alerts: #14, #17, #22, #23, #37, #38, #39 - js/insecure-randomness: replace Math.random with crypto.randomInt in util - js/http-to-file-access: validate output filename before file write - js/indirect-command-line-injection: sanitize branch name for shell - js/incomplete-sanitization: replace all apostrophes, not first only - js/prototype-polluting-assignment: skip __proto__/constructor/prototype - js/insufficient-password-hash (#41): false positive, HMAC-SHA256 correct for API signing --- .../bootstrap/src/lib/modules/overrider.ts | 1 + packages/core/bootstrap/src/lib/util.ts | 5 +++-- .../core/bootstrap/test/unit/utils.test.ts | 9 +++++---- packages/observation/index.ts | 15 +++++++++++++-- packages/scripts/src/workspace.ts | 18 ++++++++++++------ .../src/transport/addressManager.ts | 2 +- 6 files changed, 35 insertions(+), 15 deletions(-) diff --git a/packages/core/bootstrap/src/lib/modules/overrider.ts b/packages/core/bootstrap/src/lib/modules/overrider.ts index e34a5ef209..638282db2f 100644 --- a/packages/core/bootstrap/src/lib/modules/overrider.ts +++ b/packages/core/bootstrap/src/lib/modules/overrider.ts @@ -116,6 +116,7 @@ export class Overrider { ): AdapterOverrides => { const combinedOverrides = internalOverrides || {} for (const symbol of Object.keys(inputOverrides)) { + if (symbol === '__proto__' || symbol === 'constructor' || symbol === 'prototype') continue combinedOverrides[symbol] = inputOverrides[symbol] } return combinedOverrides diff --git a/packages/core/bootstrap/src/lib/util.ts b/packages/core/bootstrap/src/lib/util.ts index f010365d4f..227fab7b45 100644 --- a/packages/core/bootstrap/src/lib/util.ts +++ b/packages/core/bootstrap/src/lib/util.ts @@ -1,3 +1,4 @@ +import crypto from 'crypto' import type { AdapterContext, AdapterImplementation, @@ -93,7 +94,7 @@ export const getRandomEnv = (name: string, delimiter = ',', prefix = ''): string const val = getEnv(name, prefix) if (!val) return val const items = val.split(delimiter) - return items[Math.floor(Math.random() * items.length)] + return items[crypto.randomInt(items.length)] } // pick a random string from env var after splitting with the delimiter ("a&b&c" "&" -> choice(["a","b","c"])) @@ -104,7 +105,7 @@ export const getRandomRequiredEnv = ( ): string | undefined => { const val = getRequiredEnv(name, prefix) const items = val.split(delimiter) - return items[Math.floor(Math.random() * items.length)] + return items[crypto.randomInt(items.length)] } // We generate an UUID per instance diff --git a/packages/core/bootstrap/test/unit/utils.test.ts b/packages/core/bootstrap/test/unit/utils.test.ts index d00e585d1f..b9bd3e750f 100644 --- a/packages/core/bootstrap/test/unit/utils.test.ts +++ b/packages/core/bootstrap/test/unit/utils.test.ts @@ -1,3 +1,4 @@ +import crypto from 'crypto' import { AdapterContext, AdapterRequest, @@ -200,10 +201,10 @@ describe('utils', () => { const varName = 'RANDOM_TEST_ENV_VAR' process.env[varName] = 'one,two,three' jest - .spyOn(global.Math, 'random') - .mockReturnValueOnce(0.1) - .mockReturnValueOnce(0.5) - .mockReturnValueOnce(0.7) + .spyOn(crypto, 'randomInt') + .mockReturnValueOnce(0) + .mockReturnValueOnce(1) + .mockReturnValueOnce(2) expect(getRandomRequiredEnv(varName)).toBe('one') expect(getRandomRequiredEnv(varName)).toBe('two') diff --git a/packages/observation/index.ts b/packages/observation/index.ts index 5e0c6d00cd..4d205fe329 100644 --- a/packages/observation/index.ts +++ b/packages/observation/index.ts @@ -1,8 +1,18 @@ import axios from 'axios' import fs from 'fs' +import path from 'path' import { config } from './config' const HEADERS = 'round,staging,production,timestamp' + +/** Validate outputFileName to prevent path traversal; only allow safe basenames */ +function getSafeOutputPath(fileName: string): string { + const basename = path.basename(fileName) + if (basename !== fileName || basename.includes('..')) { + throw new Error(`Invalid output file name: ${fileName}`) + } + return basename +} const stagingURL = `https://adapters.main.stage.cldev.sh/${config.adapterName}` const prodURL = `https://adapters.main.prod.cldev.sh/${config.adapterName}` @@ -33,7 +43,8 @@ function sleep(ms: number) { } ;(async () => { - fs.writeFileSync(`${config.outputFileName}`, HEADERS) + const outputPath = getSafeOutputPath(config.outputFileName) + fs.writeFileSync(outputPath, HEADERS) const numRequests = config.testDurationInSeconds / config.reqIntervalInSeconds console.log(HEADERS) for (let i = 0; i < numRequests; i++) { @@ -41,7 +52,7 @@ function sleep(ms: number) { let content = `\n${i}` content += `, ${result.stagingResult}, ${result.prodResult}, ${new Date().toISOString()}` console.log(content) - fs.appendFileSync(`${config.outputFileName}`, content) + fs.appendFileSync(outputPath, content) await sleep(config.reqIntervalInSeconds * 1000) } })() diff --git a/packages/scripts/src/workspace.ts b/packages/scripts/src/workspace.ts index 0bcb4f7bba..604cf63de9 100644 --- a/packages/scripts/src/workspace.ts +++ b/packages/scripts/src/workspace.ts @@ -32,15 +32,21 @@ export const PUBLIC_ADAPTER_TYPES = [ ] const scope = '@chainlink/' +/** Sanitize branch name for safe use in shell; only allow alphanumeric, /, -, _, . */ +function sanitizeBranchForShell(branch: string): string { + if (!/^[a-zA-Z0-9/_.-]*$/.test(branch)) { + throw new Error(`Invalid branch name: contains disallowed characters`) + } + return branch +} + export type WorkspacePackages = ReturnType export function getWorkspacePackages(changedFromBranch = ''): WorkspacePackage[] { + const sinceArg = changedFromBranch + ? ` --since=${sanitizeBranchForShell(changedFromBranch)}` + : '' return s - .exec( - changedFromBranch - ? `yarn workspaces list -R --json --since=${changedFromBranch}` - : 'yarn workspaces list -R --json', - { silent: true }, - ) + .exec(`yarn workspaces list -R --json${sinceArg}`.trim(), { silent: true }) .split('\n') .filter(Boolean) .map((v) => { diff --git a/packages/sources/por-address-list/src/transport/addressManager.ts b/packages/sources/por-address-list/src/transport/addressManager.ts index 4aa6ebe0da..cf0e9840dd 100644 --- a/packages/sources/por-address-list/src/transport/addressManager.ts +++ b/packages/sources/por-address-list/src/transport/addressManager.ts @@ -96,7 +96,7 @@ export class LombardAddressManager extends AddressManager r[0]) .filter((address) => address != '') .map((address) => ({ - address: address.replace("'", ''), + address: address.replace(/'/g, ''), network: network, chainId: chainId, })) From 40bf437cd13f8d005cc4affeaf4e4e06d3478c25 Mon Sep 17 00:00:00 2001 From: Eric Fornaciari Date: Fri, 20 Feb 2026 09:31:29 -0800 Subject: [PATCH 2/4] chore: add changeset for CodeQL security fixes --- .changeset/swift-dodos-fix.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .changeset/swift-dodos-fix.md diff --git a/.changeset/swift-dodos-fix.md b/.changeset/swift-dodos-fix.md new file mode 100644 index 0000000000..1bcd01f9d4 --- /dev/null +++ b/.changeset/swift-dodos-fix.md @@ -0,0 +1,14 @@ +--- +'@chainlink/ea-bootstrap': patch +'@chainlink/observation': patch +'@chainlink/ea-scripts': patch +'@chainlink/por-address-list-adapter': patch +--- + +fix(security): resolve CodeQL code alerts + +- js/insecure-randomness: replace Math.random with crypto.randomInt in util +- js/http-to-file-access: validate output filename before file write +- js/indirect-command-line-injection: sanitize branch name for shell +- js/incomplete-sanitization: replace all apostrophes, not first only +- js/prototype-polluting-assignment: skip __proto__/constructor/prototype From c63a3b0ac40d082402a765f5314aa5ace3e3203a Mon Sep 17 00:00:00 2001 From: Eric Fornaciari Date: Fri, 20 Feb 2026 09:43:22 -0800 Subject: [PATCH 3/4] fix(ci): resolve TypeScript and Prettier issues - utils.test.ts: use mockImplementationOnce for crypto.randomInt spy to fix TS2345 - swift-dodos-fix.md: escape __proto__ in markdown for Prettier --- .changeset/swift-dodos-fix.md | 2 +- packages/core/bootstrap/test/unit/utils.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.changeset/swift-dodos-fix.md b/.changeset/swift-dodos-fix.md index 1bcd01f9d4..3d103b9f30 100644 --- a/.changeset/swift-dodos-fix.md +++ b/.changeset/swift-dodos-fix.md @@ -11,4 +11,4 @@ fix(security): resolve CodeQL code alerts - js/http-to-file-access: validate output filename before file write - js/indirect-command-line-injection: sanitize branch name for shell - js/incomplete-sanitization: replace all apostrophes, not first only -- js/prototype-polluting-assignment: skip __proto__/constructor/prototype +- js/prototype-polluting-assignment: skip `__proto__`/constructor/prototype diff --git a/packages/core/bootstrap/test/unit/utils.test.ts b/packages/core/bootstrap/test/unit/utils.test.ts index b9bd3e750f..d3ff62ac14 100644 --- a/packages/core/bootstrap/test/unit/utils.test.ts +++ b/packages/core/bootstrap/test/unit/utils.test.ts @@ -202,9 +202,9 @@ describe('utils', () => { process.env[varName] = 'one,two,three' jest .spyOn(crypto, 'randomInt') - .mockReturnValueOnce(0) - .mockReturnValueOnce(1) - .mockReturnValueOnce(2) + .mockImplementationOnce(() => 0) + .mockImplementationOnce(() => 1) + .mockImplementationOnce(() => 2) expect(getRandomRequiredEnv(varName)).toBe('one') expect(getRandomRequiredEnv(varName)).toBe('two') From fc8fdf0665fbe8084058ea9f6d12b486921555bc Mon Sep 17 00:00:00 2001 From: Eric Fornaciari Date: Fri, 20 Feb 2026 09:57:24 -0800 Subject: [PATCH 4/4] fix(ci): format workspace.ts with Prettier --- packages/scripts/src/workspace.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/scripts/src/workspace.ts b/packages/scripts/src/workspace.ts index 604cf63de9..53d9f990aa 100644 --- a/packages/scripts/src/workspace.ts +++ b/packages/scripts/src/workspace.ts @@ -42,9 +42,7 @@ function sanitizeBranchForShell(branch: string): string { export type WorkspacePackages = ReturnType export function getWorkspacePackages(changedFromBranch = ''): WorkspacePackage[] { - const sinceArg = changedFromBranch - ? ` --since=${sanitizeBranchForShell(changedFromBranch)}` - : '' + const sinceArg = changedFromBranch ? ` --since=${sanitizeBranchForShell(changedFromBranch)}` : '' return s .exec(`yarn workspaces list -R --json${sinceArg}`.trim(), { silent: true }) .split('\n')