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>
This commit is contained in:
parent
69b2f3f90b
commit
37b4536580
24 changed files with 497 additions and 56 deletions
|
|
@ -1,7 +1,6 @@
|
|||
import cp, { type ExecException } from 'node:child_process'
|
||||
import url from 'node:url'
|
||||
import path from 'node:path'
|
||||
import util from 'node:util'
|
||||
import chalk from 'chalk'
|
||||
import { rimraf } from 'rimraf'
|
||||
import { copy } from 'esbuild-plugin-copy'
|
||||
|
|
@ -10,7 +9,6 @@ import type { PackageJson } from 'type-fest'
|
|||
|
||||
const MAX_RETRIES = 3
|
||||
const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
|
||||
const exec = util.promisify(cp.exec)
|
||||
const tscPath = path.resolve(path.dirname(url.fileURLToPath(import.meta.resolve('typescript'))), '..', 'bin', 'tsc')
|
||||
|
||||
const l = {
|
||||
|
|
@ -74,8 +72,36 @@ export function clear(config: BuildOptions): Plugin {
|
|||
* @return {Promise<void>} A promise that resolves when the types are generated.
|
||||
*/
|
||||
function generateTypes(cwd: string, pkg: PackageJson, retry = MAX_RETRIES): Promise<void> {
|
||||
const child = exec(`node ${tscPath} --emitDeclarationOnly`, { cwd })
|
||||
return child.then(() => { }, (err) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const child = cp.spawn('node', [tscPath, '--emitDeclarationOnly'], { cwd })
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
|
||||
child.stdout?.on('data', (data) => {
|
||||
stdout += data
|
||||
})
|
||||
|
||||
child.stderr?.on('data', (data) => {
|
||||
stderr += data
|
||||
})
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve(undefined)
|
||||
} else {
|
||||
const error = new Error(`TypeScript compilation failed with code ${code}`)
|
||||
// @ts-ignore
|
||||
error.stdout = stdout
|
||||
// @ts-ignore
|
||||
error.stderr = stderr
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
|
||||
child.on('error', (error) => {
|
||||
reject(error)
|
||||
})
|
||||
}).catch((err) => {
|
||||
if (retry > 0) {
|
||||
console.log(`${l.name(pkg.name)} ↻ Retrying (${MAX_RETRIES - (retry - 1)}/${MAX_RETRIES}) building types for ${pkg.name}`)
|
||||
return generateTypes(cwd, pkg, retry - 1)
|
||||
|
|
|
|||
|
|
@ -437,7 +437,7 @@ export const QUESTIONNAIRE = [{
|
|||
default: 'LT_USERNAME',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => (
|
||||
answers.backend && answers.backend.indexOf('LambdaTest') > -1 &&
|
||||
answers.hostname!.indexOf('lambdatest.com') > -1
|
||||
(answers.hostname === 'lambdatest.com' || answers.hostname?.endsWith('.lambdatest.com'))
|
||||
)
|
||||
}, {
|
||||
type: 'input',
|
||||
|
|
@ -446,7 +446,7 @@ export const QUESTIONNAIRE = [{
|
|||
default: 'LT_ACCESS_KEY',
|
||||
when: /* istanbul ignore next */ (answers: Questionnair) => (
|
||||
answers.backend && answers.backend.indexOf('LambdaTest') > -1 &&
|
||||
answers.hostname!.indexOf('lambdatest.com') > -1
|
||||
(answers.hostname === 'lambdatest.com' || answers.hostname?.endsWith('.lambdatest.com'))
|
||||
)
|
||||
}, {
|
||||
type: 'input',
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class AiHandler {
|
|||
const sessionId = browser.sessionId
|
||||
|
||||
// a utility function to escape single and double quotes
|
||||
const escapeString = (str: string) => str.replace(/'/g, "\\'").replace(/"/g, '\\"')
|
||||
const escapeString = (str: string) => str.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t')
|
||||
|
||||
const tcgDetails = escapeString(JSON.stringify({
|
||||
region: TCG_INFO.tcgRegion,
|
||||
|
|
|
|||
|
|
@ -1129,7 +1129,7 @@ export function isBrowserstackInfra(config: BrowserstackConfig & Options.Testrun
|
|||
// In case hostname is not present anywhere in the config, it returns true by default as hostname is not a mandatory parameter in the config
|
||||
|
||||
const isBrowserstack = (str: string ): boolean => {
|
||||
return str.includes('browserstack.com')
|
||||
return str === 'browserstack.com' || str.endsWith('.browserstack.com')
|
||||
}
|
||||
|
||||
if ((config.hostname) && !isBrowserstack(config.hostname)) {
|
||||
|
|
@ -1652,12 +1652,27 @@ export function getBooleanValueFromString(value: string | undefined): boolean {
|
|||
return ['true'].includes(value.trim().toLowerCase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a key is safe to use for object property assignment to prevent prototype pollution
|
||||
* @param key - The key to check
|
||||
* @returns true if the key is safe, false otherwise
|
||||
*/
|
||||
function isSafeKey(key: string): boolean {
|
||||
const dangerousKeys = ['__proto__', 'constructor', 'prototype']
|
||||
return !dangerousKeys.includes(key)
|
||||
}
|
||||
|
||||
export function mergeDeep(target: Record<string, any>, ...sources: any[]): Record<string, any> {
|
||||
if (!sources.length) {return target}
|
||||
const source = sources.shift()
|
||||
|
||||
if (isObject(target) && isObject(source)) {
|
||||
for (const key in source) {
|
||||
// Skip dangerous keys that could lead to prototype pollution
|
||||
if (!isSafeKey(key)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const sourceValue = source[key]
|
||||
const targetValue = target[key]
|
||||
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ export function generateSkipTagsFromCapabilities(capabilities: Capabilities.Reso
|
|||
.find((filter: WebdriverIO.Capabilities) => Object.keys(filter)
|
||||
.every((key: keyof WebdriverIO.Capabilities) => match((capabilities as Record<string, string>)[key], filter[key] as RegExp)))
|
||||
if (isSkip) {
|
||||
generatedTags.push(`(not ${tag.replace(/(\(|\))/g, '\\$1')})`)
|
||||
generatedTags.push(`(not ${tag.replace(/[()[\]{}^$*+?.|\\]/g, '\\$&')})`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@
|
|||
"typeScriptVersion": "3.8.3",
|
||||
"dependencies": {
|
||||
"@wdio/reporter": "workspace:*",
|
||||
"@wdio/types": "workspace:*"
|
||||
"@wdio/types": "workspace:*",
|
||||
"safe-regex2": "^5.0.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
import safeRegexTest from 'safe-regex2'
|
||||
|
||||
import type { ResultSet } from './types.js'
|
||||
|
||||
const DEFAULT_FILENAME = 'wdio-merged.json'
|
||||
|
|
@ -28,7 +31,25 @@ export default async function mergeResults(
|
|||
}
|
||||
|
||||
async function getDataFromFiles (dir: string, filePattern: string | RegExp) {
|
||||
const fileNames = (await fs.readdir(dir)).filter((file) => file.match(filePattern))
|
||||
let safePattern: RegExp
|
||||
|
||||
if (filePattern instanceof RegExp) {
|
||||
// For existing RegExp objects, test them for safety
|
||||
safePattern = safeRegexTest(filePattern) ? filePattern : /\.json$/
|
||||
} else if (typeof filePattern === 'string') {
|
||||
try {
|
||||
// Test the created RegExp for safety
|
||||
safePattern = safeRegexTest(filePattern) ? new RegExp(filePattern) : /\.json$/
|
||||
} catch {
|
||||
// If the pattern syntax is invalid, fall back to a safe default
|
||||
safePattern = /\.json$/
|
||||
}
|
||||
} else {
|
||||
// If pattern is unsafe or invalid, use a safe default
|
||||
safePattern = /\.json$/
|
||||
}
|
||||
|
||||
const fileNames = (await fs.readdir(dir)).filter((file) => file.match(safePattern))
|
||||
const data: unknown[] = []
|
||||
|
||||
await Promise.all(fileNames.map(async (fileName) => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "./build",
|
||||
"rootDir": "./src"
|
||||
"rootDir": "./src",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
"chalk": "^5.1.2",
|
||||
"loglevel": "^1.6.0",
|
||||
"loglevel-plugin-prefix": "^0.8.4",
|
||||
"safe-regex2": "^5.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import safeRegexTest from 'safe-regex2'
|
||||
|
||||
export const SENSITIVE_DATA_REPLACER = '**MASKED**'
|
||||
|
||||
const skipError = (aFunction: Function) => {
|
||||
|
|
@ -21,15 +23,20 @@ export const parseMaskingPatterns = (maskingRegexString: string | undefined) =>
|
|||
|
||||
return regexStrings?.map((regexStr) => {
|
||||
const regexParts = regexStr.match(/^\/(.*?)\/([gimsuy]*)$/)
|
||||
if (!regexParts) {
|
||||
if (!regexParts && safeRegexTest(regexStr)) {
|
||||
// When passing only a simple string without `/` or flags, aka `(--key=)([^ ]*)`
|
||||
return skipError(() => new RegExp(regexStr))
|
||||
} else if (regexParts?.[1]) {
|
||||
// Case with flag `/(--key=)([^ ]*)/i` or without flag `/(--key=)([^ ]*)/`
|
||||
return skipError(() => regexParts[2] ? new RegExp(regexParts[1], regexParts[2]) : new RegExp(regexParts[1]))
|
||||
}
|
||||
return undefined
|
||||
|
||||
if (regexParts?.[1] && safeRegexTest(regexParts[1])) {
|
||||
// Case with flag `/(--key=)([^ ]*)/i` or without flag `/(--key=)([^ ]*)/`
|
||||
return skipError(() => (
|
||||
regexParts[2]
|
||||
? new RegExp(regexParts[1], regexParts[2])
|
||||
: new RegExp(regexParts[1])))
|
||||
}
|
||||
|
||||
return undefined
|
||||
}).filter((regex) => regex !== undefined)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -229,4 +229,4 @@ describe('wdio-logger utils', () => {
|
|||
expect(maskedValue).toEqual('RESULT **MASKED**\n')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "./build",
|
||||
"rootDir": "./src"
|
||||
"rootDir": "./src",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ export function requireExternalModules (mods: string[], loader = loadModule) {
|
|||
return Promise.resolve()
|
||||
}
|
||||
|
||||
mod = mod.replace(/.*:/, '')
|
||||
mod = mod.includes(':') ? mod.substring(mod.lastIndexOf(':') + 1) : mod
|
||||
|
||||
if (mod.startsWith('./') && globalThis.process) {
|
||||
mod = `${globalThis.process.cwd()}/${mod.slice(2)}`
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@
|
|||
"@wdio/types": "workspace:*",
|
||||
"decamelize": "^6.0.0",
|
||||
"deepmerge-ts": "^7.0.3",
|
||||
"edgedriver": "^6.1.1",
|
||||
"edgedriver": "^6.1.2",
|
||||
"geckodriver": "^5.0.0",
|
||||
"get-port": "^7.0.0",
|
||||
"import-meta-resolve": "^4.0.0",
|
||||
|
|
|
|||
|
|
@ -66,10 +66,18 @@ export function getBuildIdByChromePath(chromePath?: string) {
|
|||
return oldest
|
||||
}
|
||||
|
||||
const versionString = cp.execSync(`"${chromePath}" --version --no-sandbox`).toString()
|
||||
const versionSanitized = versionString.trim().split(' ').find((s) => s.split('.').length === 4)
|
||||
const result = cp.spawnSync(chromePath, ['--version', '--no-sandbox'], {
|
||||
encoding: 'utf8',
|
||||
env: process.env
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
throw result.error
|
||||
}
|
||||
|
||||
const versionSanitized = result.stdout.trim().split(' ').find((s) => s.split('.').length === 4)
|
||||
if (!versionSanitized) {
|
||||
throw new Error(`Couldn't find valid Chrome version from "${versionString}", please raise an issue in the WebdriverIO project (https://github.com/webdriverio/webdriverio/issues/new/choose)`)
|
||||
throw new Error(`Couldn't find valid Chrome version from "${result.stdout}", please raise an issue in the WebdriverIO project (https://github.com/webdriverio/webdriverio/issues/new/choose)`)
|
||||
}
|
||||
return versionSanitized
|
||||
}
|
||||
|
|
@ -85,12 +93,20 @@ export async function getBuildIdByFirefoxPath(firefoxPath?: string) {
|
|||
return contents
|
||||
.split('\n')
|
||||
.filter((line) => line.startsWith('Version='))
|
||||
.map((line) => line.replace('Version=', '').replace(/\r/, ''))
|
||||
.map((line) => line.replace(/Version=/g, '').replace(/\r/g, ''))
|
||||
.pop()
|
||||
}
|
||||
|
||||
const versionString = cp.execSync(`"${firefoxPath}" --version`).toString()
|
||||
return versionString.trim().split(' ').pop()?.trim()
|
||||
const result = cp.spawnSync(firefoxPath, ['--version'], {
|
||||
encoding: 'utf8',
|
||||
env: process.env
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
throw result.error
|
||||
}
|
||||
|
||||
return result.stdout.trim().split(' ').pop()?.trim()
|
||||
}
|
||||
|
||||
let lastTimeCalled = Date.now()
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ export function commandCallStructure (commandName: string, args: unknown[], unfu
|
|||
* - Returned async functions: `return async function` or `return (async function`
|
||||
* - Arrow functions: `() =>` or `param =>` or `(param1, param2) =>`
|
||||
*/
|
||||
/^\s*(?:(?:async\s+)?function\s*[\w\s]*\(|!function\(|return\s+\(?(?:async\s+)?function|\(?\w*(?:\s*,\s*\w+)*\)?\s*=>)/.test(arg.trim())
|
||||
/^\s*(?:(?:async\s+)?function(?:\s+\w+)?\s*\(|!function\(|return\s+\(?(?:async\s+)?function|\([^)]*\)\s*=>|\w+\s*=>)/.test(arg.trim())
|
||||
) {
|
||||
arg = '<fn>'
|
||||
} else if (
|
||||
|
|
|
|||
|
|
@ -59,7 +59,8 @@ vi.mock('node:fs/promises', () => ({
|
|||
|
||||
vi.mock('node:child_process', () => ({
|
||||
default: {
|
||||
execSync: vi.fn()
|
||||
execSync: vi.fn(),
|
||||
spawnSync: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
|
|
@ -75,7 +76,14 @@ vi.mock('@puppeteer/browsers', () => ({
|
|||
|
||||
describe('driver utils', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(cp.execSync).mockReturnValue(Buffer.from('Google Chrome 116.0.5845.110 \n'))
|
||||
vi.mocked(cp.spawnSync).mockReturnValue({
|
||||
pid: 123,
|
||||
output: [],
|
||||
stdout: 'Google Chrome 116.0.5845.110 \n',
|
||||
stderr: '',
|
||||
status: 0,
|
||||
signal: null
|
||||
})
|
||||
})
|
||||
|
||||
it('should parse params', () => {
|
||||
|
|
@ -86,10 +94,24 @@ describe('driver utils', () => {
|
|||
it('getBuildIdByChromePath', () => {
|
||||
expect(getBuildIdByChromePath()).toBe(undefined)
|
||||
expect(getBuildIdByChromePath('/foo/bar')).toBe('116.0.5845.110')
|
||||
expect(cp.execSync).toBeCalledWith('"/foo/bar" --version --no-sandbox')
|
||||
vi.mocked(cp.execSync).mockReturnValue(Buffer.from('Chromium 117.0.5938.88 Fedora Project \n'))
|
||||
expect(cp.spawnSync).toBeCalledWith('/foo/bar', ['--version', '--no-sandbox'], expect.any(Object))
|
||||
vi.mocked(cp.spawnSync).mockReturnValue({
|
||||
pid: 123,
|
||||
output: [],
|
||||
stdout: 'Chromium 117.0.5938.88 Fedora Project \n',
|
||||
stderr: '',
|
||||
status: 0,
|
||||
signal: null
|
||||
})
|
||||
expect(getBuildIdByChromePath('/foo/bar')).toBe('117.0.5938.88')
|
||||
vi.mocked(cp.execSync).mockReturnValue(Buffer.from('Chromium 117.0.5938.92 snap \n'))
|
||||
vi.mocked(cp.spawnSync).mockReturnValue({
|
||||
pid: 123,
|
||||
output: [],
|
||||
stdout: 'Chromium 117.0.5938.92 snap \n',
|
||||
stderr: '',
|
||||
status: 0,
|
||||
signal: null
|
||||
})
|
||||
expect(getBuildIdByChromePath('/foo/bar')).toBe('117.0.5938.92')
|
||||
vi.mocked(os.platform).mockReturnValueOnce('win32')
|
||||
expect(getBuildIdByChromePath('/foo/bar')).toBe('115.0.5790.110')
|
||||
|
|
|
|||
|
|
@ -376,7 +376,7 @@ export const getSessionError = (err: JSONWPCommandError, params: Partial<Options
|
|||
'\nIf you use a grid server ' + w3cCapMessage
|
||||
}
|
||||
|
||||
if (err.message.includes('failed serving request POST /wd/hub/session: Unauthorized') && params.hostname?.endsWith('saucelabs.com')) {
|
||||
if (err.message.includes('failed serving request POST /wd/hub/session: Unauthorized') && (params.hostname === 'saucelabs.com' || params.hostname?.endsWith('.saucelabs.com'))) {
|
||||
return 'Session request was not authorized because you either did provide a wrong access key or tried to run ' +
|
||||
'in a region that has not been enabled for your user. If have registered a free trial account it is connected ' +
|
||||
'to a specific region. Ensure this region is set in your configuration (https://webdriver.io/docs/options.html#region).'
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export * from './element/getElement.js'
|
|||
* don't expose this method as it should not be attached to the `WebdriverIO.Element` prototype.
|
||||
*/
|
||||
// export * from './element/getElements.js'
|
||||
export * from './element/getHTML.js'
|
||||
export { getHTML } from './element/getHTML.js'
|
||||
export * from './element/getLocation.js'
|
||||
export * from './element/getProperty.js'
|
||||
export * from './element/getSize.js'
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ function populateHTML (
|
|||
* @param options command options
|
||||
* @returns a string with the cleaned up HTML
|
||||
*/
|
||||
function sanitizeHTML ($: CheerioAPI | string, options: GetHTMLOptions = {}): string {
|
||||
export function sanitizeHTML ($: CheerioAPI | string, options: GetHTMLOptions = {}): string {
|
||||
/**
|
||||
* delete data-wdio-shadow-id attribute as it contains random ids that
|
||||
* can cause failures when taking a snapshot of a Shadow DOM element
|
||||
|
|
@ -215,12 +215,31 @@ function sanitizeHTML ($: CheerioAPI | string, options: GetHTMLOptions = {}): st
|
|||
for (const elemToRemove of (options.excludeElements || [])) {
|
||||
$(elemToRemove).remove()
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove HTML comments using Cheerio's built-in functionality
|
||||
* This is more secure and reliable than regex-based removal
|
||||
*/
|
||||
if (options.removeCommentNodes) {
|
||||
// Find all comment nodes and remove them
|
||||
$('*').contents().filter(function() {
|
||||
return this.type === 'comment'
|
||||
}).remove()
|
||||
}
|
||||
}
|
||||
|
||||
let returnHTML = isCheerioObject ? $('body').html() as string : $
|
||||
if (options.removeCommentNodes) {
|
||||
returnHTML = returnHTML?.replace(/<!--[\s\S]*?-->/g, '')
|
||||
|
||||
/**
|
||||
* Fallback regex-based comment removal for string input
|
||||
* Only used when we don't have a Cheerio object
|
||||
*/
|
||||
if (!isCheerioObject && options.removeCommentNodes && returnHTML) {
|
||||
// Use a simpler, safer regex that avoids catastrophic backtracking
|
||||
// This regex matches complete HTML comments only
|
||||
returnHTML = returnHTML.replace(/<!--[\s\S]*?-->/g, '')
|
||||
}
|
||||
|
||||
return options.prettify
|
||||
? prettifyFn(returnHTML)
|
||||
: returnHTML
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const XPATH_SELECTOR_REGEXP = [
|
|||
// optional . or # + class or id
|
||||
/(?:(\.|#)(-?[_a-zA-Z]+[_a-zA-Z0-9-]*))?/,
|
||||
// optional [attribute-name="attribute-selector"]
|
||||
/(?:\[(-?[_a-zA-Z]+[_a-zA-Z0-9-]*)(?:=(?:"|')([a-zA-z0-9\-_. ]+)(?:"|'))?\])?/,
|
||||
/(?:\[(-?[_a-zA-Z]+[_a-zA-Z0-9-]*)(?:=(?:"|')([a-zA-Z0-9\-_. ]+)(?:"|'))?\])?/,
|
||||
// optional case insensitive
|
||||
/(\.)?/,
|
||||
// *=query or =query
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import path from 'node:path'
|
|||
import { expect, describe, it, afterEach, vi } from 'vitest'
|
||||
|
||||
import { remote } from '../../../src/index.js'
|
||||
import { sanitizeHTML } from '../../../src/commands/element/getHTML.js'
|
||||
|
||||
vi.mock('fetch')
|
||||
vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger')))
|
||||
|
|
@ -14,8 +15,7 @@ describe('getHTML test', () => {
|
|||
browserName: 'foobar'
|
||||
}
|
||||
})
|
||||
const elem = await browser.$('#foo')
|
||||
// @ts-expect-error mock feature
|
||||
const elem = await browser.$('#foo') as any
|
||||
elem.elementId = {
|
||||
outerHTML: '<some>outer html</some>',
|
||||
innerHTML: 'some inner html'
|
||||
|
|
@ -31,6 +31,255 @@ describe('getHTML test', () => {
|
|||
expect(result).toBe('some inner html')
|
||||
})
|
||||
|
||||
describe('sanitizeHTML', () => {
|
||||
it('should remove simple HTML comments by default', () => {
|
||||
const html = '<!-- comment -->'
|
||||
const result = sanitizeHTML(html, { removeCommentNodes: true, prettify: false })
|
||||
expect(result).toBe('')
|
||||
})
|
||||
|
||||
it('should remove comments with content around them', () => {
|
||||
const html = 'Hello <!-- comment --> World'
|
||||
const result = sanitizeHTML(html, { removeCommentNodes: true, prettify: false })
|
||||
expect(result).toBe('Hello World')
|
||||
})
|
||||
|
||||
it('should preserve comments when removeCommentNodes is false', () => {
|
||||
const html = 'Hello <!-- comment --> World'
|
||||
const result = sanitizeHTML(html, { removeCommentNodes: false, prettify: false })
|
||||
expect(result).toBe('Hello <!-- comment --> World')
|
||||
})
|
||||
|
||||
it('should remove multiple comments', () => {
|
||||
const html = '<!-- start -->Text<!-- middle -->More<!-- end -->'
|
||||
const result = sanitizeHTML(html, { removeCommentNodes: true, prettify: false })
|
||||
expect(result).toBe('TextMore')
|
||||
})
|
||||
|
||||
it('should handle framework-specific comments (e.g., Lit)', () => {
|
||||
const html = 'Content<!--?lit$206212805$--><!--?lit$206212805$-->After'
|
||||
const result = sanitizeHTML(html, { removeCommentNodes: true, prettify: false })
|
||||
expect(result).toBe('ContentAfter')
|
||||
})
|
||||
|
||||
it('should handle multiline comments', () => {
|
||||
const html = `Text<!--
|
||||
multiline
|
||||
comment
|
||||
-->End`
|
||||
const result = sanitizeHTML(html, { removeCommentNodes: true, prettify: false })
|
||||
expect(result).toBe('TextEnd')
|
||||
})
|
||||
|
||||
it('should handle multiline comments with various content', () => {
|
||||
const html = `<div>
|
||||
<!-- This is a comment
|
||||
that spans multiple lines
|
||||
and contains various content:
|
||||
- List items
|
||||
- Special chars: <>&'"
|
||||
- Even code: function() { return true; }
|
||||
-->
|
||||
<p>Content</p>
|
||||
</div>`
|
||||
const result = sanitizeHTML(html, { removeCommentNodes: true, prettify: false })
|
||||
expect(result).not.toContain('<!--')
|
||||
expect(result).toContain('<p>Content</p>')
|
||||
expect(result).not.toContain('List items')
|
||||
expect(result).not.toContain('Special chars')
|
||||
})
|
||||
|
||||
it('should handle multiple multiline comments', () => {
|
||||
const html = `Start
|
||||
<!--
|
||||
First multiline
|
||||
comment block
|
||||
-->
|
||||
Middle
|
||||
<!--
|
||||
Second multiline
|
||||
comment block
|
||||
-->
|
||||
End`
|
||||
const result = sanitizeHTML(html, { removeCommentNodes: true, prettify: false })
|
||||
expect(result.replace(/\s+/g, ' ').trim()).toBe('Start Middle End')
|
||||
})
|
||||
|
||||
it('should handle multiline comments with nested HTML-like content', () => {
|
||||
const html = `<div>
|
||||
<!--
|
||||
<div class="commented-out">
|
||||
<p>This HTML is commented out</p>
|
||||
<script>console.log('also commented')</script>
|
||||
</div>
|
||||
-->
|
||||
<p>This is visible</p>
|
||||
</div>`
|
||||
const result = sanitizeHTML(html, { removeCommentNodes: true, prettify: false })
|
||||
expect(result).not.toContain('commented-out')
|
||||
expect(result).not.toContain('console.log')
|
||||
expect(result).toContain('<p>This is visible</p>')
|
||||
})
|
||||
|
||||
it('should handle Windows-style line endings in multiline comments', () => {
|
||||
const html = 'Before<!--\r\nWindows\r\nstyle\r\ncomment\r\n-->After'
|
||||
const result = sanitizeHTML(html, { removeCommentNodes: true, prettify: false })
|
||||
expect(result).toBe('BeforeAfter')
|
||||
})
|
||||
|
||||
it('should handle mixed line endings in multiline comments', () => {
|
||||
const html = 'Text<!--\nUnix line\r\nWindows line\rOld Mac line-->End'
|
||||
const result = sanitizeHTML(html, { removeCommentNodes: true, prettify: false })
|
||||
expect(result).toBe('TextEnd')
|
||||
})
|
||||
|
||||
it('should handle multiline comments in Cheerio mode', async () => {
|
||||
const { load } = await import('cheerio')
|
||||
const html = `
|
||||
<html>
|
||||
<body>
|
||||
<!--
|
||||
This is a multiline comment
|
||||
in the document
|
||||
-->
|
||||
<div>
|
||||
Content
|
||||
<!--
|
||||
Another multiline
|
||||
comment inside div
|
||||
-->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
const $ = load(html)
|
||||
const result = sanitizeHTML($, { removeCommentNodes: true, prettify: false })
|
||||
expect(result).not.toContain('<!--')
|
||||
expect(result).not.toContain('multiline comment')
|
||||
expect(result).toContain('<div>')
|
||||
expect(result).toContain('Content')
|
||||
})
|
||||
|
||||
it('should handle comments with special characters', () => {
|
||||
const html = 'Before<!-- comment with <tags> & special chars -->After'
|
||||
const result = sanitizeHTML(html, { removeCommentNodes: true, prettify: false })
|
||||
expect(result).toBe('BeforeAfter')
|
||||
})
|
||||
|
||||
it('should handle empty comments', () => {
|
||||
const html = 'Text<!---->More'
|
||||
const result = sanitizeHTML(html, { removeCommentNodes: true, prettify: false })
|
||||
expect(result).toBe('TextMore')
|
||||
})
|
||||
|
||||
it('should not be vulnerable to ReDoS attacks', () => {
|
||||
// Create a string that could cause catastrophic backtracking in vulnerable regex
|
||||
const maliciousInput = '<!--' + 'a'.repeat(10000) + '-->' + '<!--' + 'b'.repeat(10000) + '-->'
|
||||
|
||||
const startTime = Date.now()
|
||||
const result = sanitizeHTML(maliciousInput, { removeCommentNodes: true, prettify: false })
|
||||
const executionTime = Date.now() - startTime
|
||||
|
||||
// Should complete quickly (under 50ms) even with malicious input
|
||||
expect(executionTime).toBeLessThan(50)
|
||||
// The simplified regex should handle it gracefully
|
||||
expect(result).not.toContain('<!--')
|
||||
expect(result).toBe('')
|
||||
})
|
||||
|
||||
it('should handle nested comment-like structures', () => {
|
||||
// Note: In valid HTML, comments cannot be nested, but we should handle malformed input gracefully
|
||||
const html = '<!-- outer <!-- inner --> still in comment --> Text'
|
||||
const result = sanitizeHTML(html, { removeCommentNodes: true, prettify: false })
|
||||
expect(result).toBe(' still in comment --> Text')
|
||||
})
|
||||
|
||||
it('should handle incomplete comments', () => {
|
||||
// Comments without closing tags should remain as-is (safer than trying to guess)
|
||||
const html = 'Text <!--incomplete comment'
|
||||
const result = sanitizeHTML(html, { removeCommentNodes: true, prettify: false })
|
||||
expect(result).toBe('Text <!--incomplete comment')
|
||||
})
|
||||
|
||||
it('should remove comments in complex HTML when using Cheerio', async () => {
|
||||
// Import cheerio dynamically to test the Cheerio path
|
||||
const { load } = await import('cheerio')
|
||||
const complexHtml = `
|
||||
<div>
|
||||
<!-- Comment 1 -->
|
||||
<p>Text <!-- inline comment --> content</p>
|
||||
<!-- Comment 2 -->
|
||||
<span>More<!--comment-->text</span>
|
||||
</div>
|
||||
`
|
||||
const $ = load(complexHtml)
|
||||
const result = sanitizeHTML($, { removeCommentNodes: true, prettify: false })
|
||||
|
||||
expect(result).not.toContain('<!--')
|
||||
expect(result).toContain('<p>Text content</p>')
|
||||
expect(result).toContain('<span>Moretext</span>')
|
||||
})
|
||||
|
||||
it('should exclude elements when specified', async () => {
|
||||
const { load } = await import('cheerio')
|
||||
const html = `
|
||||
<div>
|
||||
<style>body { color: red; }</style>
|
||||
<p>Keep this</p>
|
||||
<script>console.log('remove')</script>
|
||||
</div>
|
||||
`
|
||||
const $ = load(html)
|
||||
const result = sanitizeHTML($, {
|
||||
excludeElements: ['style', 'script'],
|
||||
prettify: false
|
||||
})
|
||||
|
||||
expect(result).not.toContain('<style>')
|
||||
expect(result).not.toContain('<script>')
|
||||
expect(result).toContain('<p>Keep this</p>')
|
||||
})
|
||||
|
||||
it('should remove comments and exclude elements together', async () => {
|
||||
const { load } = await import('cheerio')
|
||||
const html = `
|
||||
<div>
|
||||
<!-- Remove this comment -->
|
||||
<style>/* CSS comment */</style>
|
||||
<p>Keep this<!-- but not this --></p>
|
||||
</div>
|
||||
`
|
||||
const $ = load(html)
|
||||
const result = sanitizeHTML($, {
|
||||
removeCommentNodes: true,
|
||||
excludeElements: ['style'],
|
||||
prettify: false
|
||||
})
|
||||
|
||||
expect(result).not.toContain('<!--')
|
||||
expect(result).not.toContain('<style>')
|
||||
expect(result).toContain('<p>Keep this</p>')
|
||||
})
|
||||
|
||||
it('should handle comments at the beginning and end', () => {
|
||||
const html = '<!--start-->Middle<!--end-->'
|
||||
const result = sanitizeHTML(html, { removeCommentNodes: true, prettify: false })
|
||||
expect(result).toBe('Middle')
|
||||
})
|
||||
|
||||
it('should handle consecutive comments', () => {
|
||||
const html = 'Text<!-- comment1 --><!-- comment2 --><!-- comment3 -->End'
|
||||
const result = sanitizeHTML(html, { removeCommentNodes: true, prettify: false })
|
||||
expect(result).toBe('TextEnd')
|
||||
})
|
||||
|
||||
it('should handle HTML entities in comments', () => {
|
||||
const html = 'Text<!-- comment with <entities> -->End'
|
||||
const result = sanitizeHTML(html, { removeCommentNodes: true, prettify: false })
|
||||
expect(result).toBe('TextEnd')
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.mocked(fetch).mockClear()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1035,6 +1035,9 @@ importers:
|
|||
'@wdio/types':
|
||||
specifier: workspace:*
|
||||
version: link:../wdio-types
|
||||
safe-regex2:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
|
||||
packages/wdio-junit-reporter:
|
||||
dependencies:
|
||||
|
|
@ -1158,6 +1161,9 @@ importers:
|
|||
loglevel-plugin-prefix:
|
||||
specifier: ^0.8.4
|
||||
version: 0.8.4
|
||||
safe-regex2:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
strip-ansi:
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0
|
||||
|
|
@ -1431,8 +1437,8 @@ importers:
|
|||
specifier: ^7.0.3
|
||||
version: 7.1.5
|
||||
edgedriver:
|
||||
specifier: ^6.1.1
|
||||
version: 6.1.1
|
||||
specifier: ^6.1.2
|
||||
version: 6.1.2
|
||||
geckodriver:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
|
|
@ -7769,8 +7775,8 @@ packages:
|
|||
resolution: {integrity: sha512-sB7vSrDnFa4ezWQk9nZ/n0FdpdUuC6R1EOrlU3DL+bovcNFK28rqu2emmAUjujYEJTWIgQGqgVVWUZXMnc8iWg==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
edgedriver@6.1.1:
|
||||
resolution: {integrity: sha512-/dM/PoBf22Xg3yypMWkmRQrBKEnSyNaZ7wHGCT9+qqT14izwtFT+QvdR89rjNkMfXwW+bSFoqOfbcvM+2Cyc7w==}
|
||||
edgedriver@6.1.2:
|
||||
resolution: {integrity: sha512-UvFqd/IR81iPyWMcxXbUNi+xKWR7JjfoHjfuwjqsj9UHQKn80RpQmS0jf+U25IPi+gKVPcpOSKm0XkqgGMq4zQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
|
|
@ -8339,8 +8345,8 @@ packages:
|
|||
resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==}
|
||||
hasBin: true
|
||||
|
||||
fast-xml-parser@4.5.3:
|
||||
resolution: {integrity: sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==}
|
||||
fast-xml-parser@5.2.5:
|
||||
resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==}
|
||||
hasBin: true
|
||||
|
||||
fastq@1.19.1:
|
||||
|
|
@ -12243,6 +12249,10 @@ packages:
|
|||
resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ret@0.5.0:
|
||||
resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
retry@0.12.0:
|
||||
resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
|
||||
engines: {node: '>= 4'}
|
||||
|
|
@ -12343,6 +12353,9 @@ packages:
|
|||
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
safe-regex2@5.0.0:
|
||||
resolution: {integrity: sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==}
|
||||
|
||||
safe-stable-stringify@2.5.0:
|
||||
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -12878,6 +12891,9 @@ packages:
|
|||
strnum@1.1.2:
|
||||
resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==}
|
||||
|
||||
strnum@2.1.1:
|
||||
resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==}
|
||||
|
||||
style-to-js@1.1.17:
|
||||
resolution: {integrity: sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==}
|
||||
|
||||
|
|
@ -15863,7 +15879,7 @@ snapshots:
|
|||
'@cucumber/ci-environment': 10.0.1
|
||||
'@cucumber/cucumber-expressions': 17.1.0
|
||||
'@cucumber/gherkin': 28.0.0
|
||||
'@cucumber/gherkin-streams': 5.0.1(@cucumber/gherkin@28.0.0)(@cucumber/message-streams@4.0.1(@cucumber/messages@26.0.1))(@cucumber/messages@24.1.0)
|
||||
'@cucumber/gherkin-streams': 5.0.1(@cucumber/gherkin@28.0.0)(@cucumber/message-streams@4.0.1(@cucumber/messages@24.1.0))(@cucumber/messages@24.1.0)
|
||||
'@cucumber/gherkin-utils': 9.0.0
|
||||
'@cucumber/html-formatter': 21.6.0(@cucumber/messages@24.1.0)
|
||||
'@cucumber/message-streams': 4.0.1(@cucumber/messages@24.1.0)
|
||||
|
|
@ -15903,7 +15919,7 @@ snapshots:
|
|||
yaml: 2.8.0
|
||||
yup: 1.2.0
|
||||
|
||||
'@cucumber/gherkin-streams@5.0.1(@cucumber/gherkin@28.0.0)(@cucumber/message-streams@4.0.1(@cucumber/messages@26.0.1))(@cucumber/messages@24.1.0)':
|
||||
'@cucumber/gherkin-streams@5.0.1(@cucumber/gherkin@28.0.0)(@cucumber/message-streams@4.0.1(@cucumber/messages@24.1.0))(@cucumber/messages@24.1.0)':
|
||||
dependencies:
|
||||
'@cucumber/gherkin': 28.0.0
|
||||
'@cucumber/message-streams': 4.0.1(@cucumber/messages@24.1.0)
|
||||
|
|
@ -19942,7 +19958,7 @@ snapshots:
|
|||
'@wdio/types': 9.16.2
|
||||
decamelize: 6.0.0
|
||||
deepmerge-ts: 7.1.5
|
||||
edgedriver: 6.1.1
|
||||
edgedriver: 6.1.2
|
||||
geckodriver: 5.0.0
|
||||
get-port: 7.1.0
|
||||
import-meta-resolve: 4.1.0
|
||||
|
|
@ -19961,7 +19977,7 @@ snapshots:
|
|||
'@wdio/types': 9.16.2
|
||||
decamelize: 6.0.0
|
||||
deepmerge-ts: 7.1.5
|
||||
edgedriver: 6.1.1
|
||||
edgedriver: 6.1.2
|
||||
geckodriver: 5.0.0
|
||||
get-port: 7.1.0
|
||||
import-meta-resolve: 4.1.0
|
||||
|
|
@ -21817,13 +21833,13 @@ snapshots:
|
|||
'@types/which': 2.0.2
|
||||
which: 2.0.2
|
||||
|
||||
edgedriver@6.1.1:
|
||||
edgedriver@6.1.2:
|
||||
dependencies:
|
||||
'@wdio/logger': 9.16.2
|
||||
'@zip.js/zip.js': 2.7.62
|
||||
decamelize: 6.0.0
|
||||
edge-paths: 3.0.5
|
||||
fast-xml-parser: 4.5.3
|
||||
fast-xml-parser: 5.2.5
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
node-fetch: 3.3.2
|
||||
|
|
@ -22646,9 +22662,9 @@ snapshots:
|
|||
dependencies:
|
||||
strnum: 1.1.2
|
||||
|
||||
fast-xml-parser@4.5.3:
|
||||
fast-xml-parser@5.2.5:
|
||||
dependencies:
|
||||
strnum: 1.1.2
|
||||
strnum: 2.1.1
|
||||
|
||||
fastq@1.19.1:
|
||||
dependencies:
|
||||
|
|
@ -27455,6 +27471,8 @@ snapshots:
|
|||
onetime: 5.1.2
|
||||
signal-exit: 3.0.7
|
||||
|
||||
ret@0.5.0: {}
|
||||
|
||||
retry@0.12.0: {}
|
||||
|
||||
retry@0.13.1: {}
|
||||
|
|
@ -27573,6 +27591,10 @@ snapshots:
|
|||
es-errors: 1.3.0
|
||||
is-regex: 1.2.1
|
||||
|
||||
safe-regex2@5.0.0:
|
||||
dependencies:
|
||||
ret: 0.5.0
|
||||
|
||||
safe-stable-stringify@2.5.0: {}
|
||||
|
||||
safe-stringify@1.2.0: {}
|
||||
|
|
@ -28208,6 +28230,8 @@ snapshots:
|
|||
|
||||
strnum@1.1.2: {}
|
||||
|
||||
strnum@2.1.1: {}
|
||||
|
||||
style-to-js@1.1.17:
|
||||
dependencies:
|
||||
style-to-object: 1.0.9
|
||||
|
|
|
|||
|
|
@ -9,6 +9,37 @@ 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) {
|
||||
|
|
@ -75,7 +106,14 @@ export default async function downloadSpec (page = 1) {
|
|||
]
|
||||
|
||||
stream.on('entry', async (entry: Entry) => {
|
||||
const unzippedFilePath = path.join(targetDir, entry.path)
|
||||
// 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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue