Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion packages/core/src/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,29 @@ export async function download({
return exec;
}

const DEFAULT_CHROMIUM_BASE_URL = 'https://storage.googleapis.com/chromium-browser-snapshots/';

// Resolve the Chromium download base URL. PERCY_CHROMIUM_BASE_URL may point at a
// private mirror, but an unvalidated value enables SSRF / an integrity downgrade
// (CWE-918): require a well-formed HTTPS URL, otherwise warn and fall back to the
// trusted default host.
export function resolveChromiumBaseUrl(value = process.env.PERCY_CHROMIUM_BASE_URL) {
if (!value) return DEFAULT_CHROMIUM_BASE_URL;
let log = logger('core:install');
let parsed;
try {
parsed = new URL(value);
} catch {
log.warn(`Invalid PERCY_CHROMIUM_BASE_URL "${value}"; using the default Chromium download host.`);
return DEFAULT_CHROMIUM_BASE_URL;
}
if (parsed.protocol !== 'https:') {
log.warn(`Ignoring non-HTTPS PERCY_CHROMIUM_BASE_URL "${value}"; Chromium must be downloaded over HTTPS.`);
return DEFAULT_CHROMIUM_BASE_URL;
}
return value.endsWith('/') ? value : `${value}/`;
}

// Installs a revision of Chromium to a local directory
export function chromium({
// default directory is within @percy/core package root
Expand All @@ -148,7 +171,7 @@ export function chromium({
} = {}) {
let extract = (i, o) => import('extract-zip').then(ex => ex.default(i, { dir: o }));

let url = (process.env.PERCY_CHROMIUM_BASE_URL || 'https://storage.googleapis.com/chromium-browser-snapshots/') +
let url = resolveChromiumBaseUrl() +
selectByPlatform({
linux: `Linux_x64/${revision}/chrome-linux.zip`,
darwin: `Mac/${revision}/chrome-mac.zip`,
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/percy.js
Original file line number Diff line number Diff line change
Expand Up @@ -863,7 +863,10 @@ export class Percy {
if (!process.env.PERCY_TOKEN) return;
try {
const logsObject = {
clilogs: logger.query(log => !['ci'].includes(log.debug))
// Redact secrets from CLI logs before egress to the Percy API — these
// can contain tokens or URLs with embedded credentials (CWE-532). The
// cilogs below were already redacted; clilogs were not.
clilogs: redactSecrets(logger.query(log => !['ci'].includes(log.debug)))
};

// Only add CI logs if not disabled voluntarily.
Expand Down
22 changes: 16 additions & 6 deletions packages/core/src/snapshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,31 @@ function validateAndFixSnapshotUrl(snapshot) {
// used to deserialize regular expression strings
const RE_REGEXP = /^\/(.+)\/(\w+)?$/;

// Upper bound on the snapshot name length we will run user-controllable
// regex/glob matching against. A crafted, very long snapshot name reaching this
// matcher (e.g. via the local API) combined with a backtracking-prone pattern
// could otherwise trigger catastrophic backtracking / ReDoS (CWE-1333). Real
// snapshot names are short; an over-long name simply does not match patterns.
const MAX_MATCH_INPUT_LENGTH = 2048;

// Returns true or false if a snapshot matches the provided include and exclude predicates. A
// predicate can be an array of predicates, a regular expression, a glob pattern, or a function.
function snapshotMatches(snapshot, include, exclude) {
// support an options object as the second argument
if (include?.include || include?.exclude) ({ include, exclude } = include);

// guard pattern matching against pathologically long inputs (ReDoS)
let patternSafe = typeof snapshot.name === 'string' && snapshot.name.length <= MAX_MATCH_INPUT_LENGTH;

// recursive predicate test function
let test = (predicate, fallback) => {
if (predicate && typeof predicate === 'string') {
// snapshot name matches exactly or matches a glob
// exact match is always safe; glob matching is only run on bounded input
let result = snapshot.name === predicate ||
micromatch.isMatch(snapshot.name, predicate);
(patternSafe && micromatch.isMatch(snapshot.name, predicate));

// snapshot might match a string-based regexp pattern
if (!result) {
// snapshot might match a string-based regexp pattern (bounded input only)
if (!result && patternSafe) {
try {
let [, parsed, flags] = RE_REGEXP.exec(predicate) || [];
result = !!parsed && new RegExp(parsed, flags).test(snapshot.name);
Expand All @@ -68,8 +78,8 @@ function snapshotMatches(snapshot, include, exclude) {

return result;
} else if (predicate instanceof RegExp) {
// snapshot matches a regular expression
return predicate.test(snapshot.name);
// snapshot matches a regular expression (bounded input only)
return patternSafe && predicate.test(snapshot.name);
} else if (typeof predicate === 'function') {
// advanced matching
return predicate(snapshot);
Expand Down
Loading