* 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>
136 lines
4.5 KiB
JavaScript
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
|
|
}
|