webdriverio/scripts/bidi/downloadSpec.ts
Christian Bromann 37b4536580
fix(security): address multiple security vulnerabilities across codebase (#14570)
* fix(security): address multiple security vulnerabilities across codebase

* further improvements

* fix unit tests

* Potential fix for code scanning alert no. 65: Incomplete multi-character sanitization

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* Potential fix for code scanning alert no. 69: Incomplete multi-character sanitization

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* properly escape regexp

* minor formatting

* improvements

* Potential fix for code scanning alert no. 72: Bad HTML filtering regexp

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* Potential fix for code scanning alert no. 76: Incomplete multi-character sanitization

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* more stable comment removal implementation

* fix tests

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-07-16 11:18:01 -07:00

136 lines
4.5 KiB
JavaScript

#!/usr/bin/env node
import fs from 'node:fs'
import fsp from 'node:fs/promises'
import url from 'node:url'
import path from 'node:path'
import { Readable } from 'node:stream'
import unzipper, { type Entry } from 'unzipper'
import { Octokit } from '@octokit/rest'
/**
* Validates and sanitizes a file path from a zip entry to prevent directory traversal attacks
* @param entryPath - The path from the zip entry
* @param targetDir - The target directory for extraction
* @returns The safe path for extraction, or null if the path is invalid
*/
function validateZipEntryPath(entryPath: string, targetDir: string): string | void {
// Normalize the entry path and remove any leading slashes
const normalizedPath = path.normalize(entryPath).replace(/^[/\\]+/, '')
// Check for directory traversal attempts
if (normalizedPath.includes('..') || path.isAbsolute(normalizedPath)) {
console.warn(`Skipping potentially dangerous path: ${entryPath}`)
return
}
// Construct the full target path
const fullPath = path.join(targetDir, normalizedPath)
// Ensure the resolved path is still within the target directory
const resolvedPath = path.resolve(fullPath)
const resolvedTargetDir = path.resolve(targetDir)
if (!resolvedPath.startsWith(resolvedTargetDir + path.sep) && resolvedPath !== resolvedTargetDir) {
console.warn(`Path traversal attempt detected: ${entryPath}`)
return
}
return fullPath
}
const MAIN_BRANCH = 'main'
export default async function downloadSpec (page = 1) {
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
const targetDir = path.join(__dirname, 'cddl')
const zipPath = path.join(targetDir, 'cddl.zip')
await fsp.rm(targetDir, { recursive: true, force: true })
await fsp.mkdir(targetDir)
/**
* check if `GITHUB_AUTH` environment variable is set to interact with GitHub API
*/
if (!process.env.GITHUB_AUTH) {
throw new Error('Please export a "GITHUB_AUTH" access token to access GitHub.')
}
const owner = 'w3c'
const repo = 'webdriver-bidi'
const api = new Octokit({ auth: process.env.GITHUB_AUTH })
const artifacts = await api.rest.actions.listArtifactsForRepo({
owner,
repo,
page,
per_page: 100
}).catch((error) => {
console.log(`Failed to download spec file: ${error.message}`)
})
if (!artifacts || artifacts.data.artifacts.length === 0) {
return false
}
// eslint-disable-next-line camelcase
const cddlBuilds = artifacts.data.artifacts.filter(({ name, workflow_run }) => (
name === 'cddl' &&
// eslint-disable-next-line camelcase
workflow_run && workflow_run.head_branch === MAIN_BRANCH
))
if (page > 10) {
return false
}
if (cddlBuilds.length === 0) {
return downloadSpec(page + 1)
}
console.log(`Downloading CDDL artifact from ${cddlBuilds[0].created_at}`)
const { data } = await api.rest.actions.downloadArtifact({
owner,
repo,
artifact_id: cddlBuilds[0].id,
archive_format: 'zip',
}) as { data: Uint8Array }
await fsp.writeFile(zipPath, Buffer.from(data))
const stream = Readable.from(fs.createReadStream(zipPath)).pipe(unzipper.Parse())
const promiseChain: Promise<string | void>[] = [
new Promise((resolve, reject) => {
stream.on('close', () => resolve())
stream.on('error', () => reject())
})
]
stream.on('entry', async (entry: Entry) => {
// Validate the entry path to prevent directory traversal attacks
const unzippedFilePath = validateZipEntryPath(entry.path, targetDir)
if (!unzippedFilePath) {
// Skip entries with invalid paths
entry.autodrain()
return
}
if (entry.type === 'Directory') {
return
}
if (!await fsp.access(path.dirname(unzippedFilePath)).then(() => true, () => false)) {
await fsp.mkdir(path.dirname(unzippedFilePath), { recursive: true })
}
const execStream = entry.pipe(fs.createWriteStream(unzippedFilePath))
console.log(`Downloading CDDL artifact to ${unzippedFilePath}`)
promiseChain.push(new Promise((resolve, reject) => {
execStream.on('close', () => resolve(unzippedFilePath))
execStream.on('error', reject)
}))
})
await Promise.all(promiseChain)
await fsp.unlink(zipPath)
return true
}