diff --git a/.changeset/swift-dodos-fix.md b/.changeset/swift-dodos-fix.md new file mode 100644 index 0000000000..3d103b9f30 --- /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 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..d3ff62ac14 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') + .mockImplementationOnce(() => 0) + .mockImplementationOnce(() => 1) + .mockImplementationOnce(() => 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..53d9f990aa 100644 --- a/packages/scripts/src/workspace.ts +++ b/packages/scripts/src/workspace.ts @@ -32,15 +32,19 @@ 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, }))