cloudinary
Advanced tools
Comparing version
@@ -0,1 +1,6 @@ | ||
2.7.0 / 2025-06-18 | ||
================== | ||
* fix: prevent parameter injection via ampersand in parameter values (#709) | ||
2.6.1 / 2025-05-05 | ||
@@ -2,0 +7,0 @@ ================== |
@@ -27,8 +27,3 @@ let PRELOADED_CLOUDINARY_PATH, config, utils; | ||
is_valid() { | ||
let expected_signature; | ||
expected_signature = utils.api_sign_request({ | ||
public_id: this.public_id, | ||
version: this.version | ||
}, config().api_secret); | ||
return this.signature === expected_signature; | ||
return utils.verify_api_response_signature(this.public_id, this.version, this.signature); | ||
} | ||
@@ -35,0 +30,0 @@ |
@@ -547,5 +547,3 @@ /** | ||
if (base_transformations.some(isObject)) { | ||
base_transformations = base_transformations.map(tr => utils.generate_transformation_string( | ||
isObject(tr) ? clone(tr) : {transformation: tr} | ||
)); | ||
base_transformations = base_transformations.map(tr => utils.generate_transformation_string(isObject(tr) ? clone(tr) : {transformation: tr})); | ||
} else { | ||
@@ -559,5 +557,3 @@ named_transformation = base_transformations.join("."); | ||
} else if (isObject(effect)) { | ||
effect = entries(effect).map( | ||
([key, value]) => `${key}:${value}` | ||
); | ||
effect = entries(effect).map(([key, value]) => `${key}:${value}`); | ||
} | ||
@@ -639,5 +635,3 @@ let border = consumeOption(options, "border"); | ||
return `${key}_${normalize_expression(value)}`; | ||
}).sort().concat( | ||
variablesParam.map(([name, value]) => `${name}_${normalize_expression(value)}`) | ||
).join(','); | ||
}).sort().concat(variablesParam.map(([name, value]) => `${name}_${normalize_expression(value)}`)).join(','); | ||
@@ -655,4 +649,3 @@ let transformations = entries(params) | ||
if (responsive_width) { | ||
let responsive_width_transformation = config().responsive_width_transformation | ||
|| DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION; | ||
let responsive_width_transformation = config().responsive_width_transformation || DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION; | ||
@@ -752,23 +745,3 @@ transformations.push(utils.generate_transformation_string(clone(responsive_width_transformation))); | ||
*/ | ||
const URL_KEYS = [ | ||
'api_secret', | ||
'auth_token', | ||
'cdn_subdomain', | ||
'cloud_name', | ||
'cname', | ||
'format', | ||
'long_url_signature', | ||
'private_cdn', | ||
'resource_type', | ||
'secure', | ||
'secure_cdn_subdomain', | ||
'secure_distribution', | ||
'shorten', | ||
'sign_url', | ||
'ssl_detected', | ||
'type', | ||
'url_suffix', | ||
'use_root_path', | ||
'version' | ||
]; | ||
const URL_KEYS = ['api_secret', 'auth_token', 'cdn_subdomain', 'cloud_name', 'cname', 'format', 'long_url_signature', 'private_cdn', 'resource_type', 'secure', 'secure_cdn_subdomain', 'secure_distribution', 'shorten', 'sign_url', 'ssl_detected', 'type', 'url_suffix', 'use_root_path', 'version']; | ||
@@ -938,5 +911,3 @@ /** | ||
let analyticsOptions = getAnalyticsOptions( | ||
Object.assign({}, options, sdkVersions) | ||
); | ||
let analyticsOptions = getAnalyticsOptions(Object.assign({}, options, sdkVersions)); | ||
@@ -1042,12 +1013,3 @@ let sdkAnalyticsSignature = getSDKAnalyticsSignature(analyticsOptions); | ||
function unsigned_url_prefix( | ||
source, | ||
cloud_name, | ||
private_cdn, | ||
cdn_subdomain, | ||
secure_cdn_subdomain, | ||
cname, | ||
secure, | ||
secure_distribution | ||
) { | ||
function unsigned_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution) { | ||
let prefix; | ||
@@ -1122,12 +1084,41 @@ if (cloud_name.indexOf("/") === 0) { | ||
function api_sign_request(params_to_sign, api_secret) { | ||
let to_sign = entries(params_to_sign).filter( | ||
([k, v]) => utils.present(v) | ||
).map( | ||
([k, v]) => `${k}=${toArray(v).join(",")}` | ||
).sort().join("&"); | ||
return compute_hash(to_sign + api_secret, config().signature_algorithm || DEFAULT_SIGNATURE_ALGORITHM, 'hex'); | ||
// Encodes a parameter for safe inclusion in URL query strings (only replaces & with %26) | ||
function encode_param(value) { | ||
return String(value).replace(/&/g, '%26'); | ||
} | ||
// Generates a string to be signed for API requests | ||
function api_string_to_sign(params_to_sign, signature_version = 2) { | ||
let params = entries(params_to_sign) | ||
.map(([k, v]) => [String(k), Array.isArray(v) ? v.join(",") : v]) | ||
.filter(([k, v]) => v !== null && v !== undefined && v !== ""); | ||
params.sort((a, b) => a[0].localeCompare(b[0])); | ||
let paramStrings = params.map(([k, v]) => { | ||
const paramString = `${k}=${v}`; | ||
return signature_version >= 2 ? encode_param(paramString) : paramString; | ||
}); | ||
return paramStrings.join("&"); | ||
} | ||
/** | ||
* Signs API request parameters | ||
* @param {Object} params_to_sign Parameters to sign | ||
* @param {string} api_secret API secret | ||
* @param {string|undefined|null} signature_algorithm Hash algorithm to use ('sha1' or 'sha256') | ||
* @param {number|undefined|null} signature_version Version of signature algorithm to use: | ||
* - Version 1: Original behavior without parameter encoding | ||
* - Version 2+ (default): Includes parameter encoding to prevent parameter smuggling | ||
* @return {string} Hexadecimal signature | ||
* @private | ||
*/ | ||
function api_sign_request(params_to_sign, api_secret, signature_algorithm = null, signature_version = null) { | ||
if (signature_version == null) { | ||
signature_version = config().signature_version || 2; | ||
} | ||
const to_sign = api_string_to_sign(params_to_sign, signature_version); | ||
const algo = signature_algorithm || config().signature_algorithm || DEFAULT_SIGNATURE_ALGORITHM; | ||
return compute_hash(to_sign + api_secret, algo, 'hex'); | ||
} | ||
/** | ||
* Computes hash from input string using specified algorithm. | ||
@@ -1150,9 +1141,5 @@ * @private | ||
let filtered_hash = {}; | ||
entries(hash).filter( | ||
([k, v]) => utils.present(v) | ||
).forEach( | ||
([k, v]) => { | ||
filtered_hash[k] = v.filter ? v.filter(x => x) : v; | ||
} | ||
); | ||
entries(hash).filter(([k, v]) => utils.present(v)).forEach(([k, v]) => { | ||
filtered_hash[k] = v.filter ? v.filter(x => x) : v; | ||
}); | ||
return filtered_hash; | ||
@@ -1175,4 +1162,6 @@ } | ||
let apiSecret = ensureOption(options, 'api_secret'); | ||
let signature_algorithm = options.signature_algorithm; | ||
let signature_version = options.signature_version; | ||
params = exports.clear_blank(params); | ||
params.signature = exports.api_sign_request(params, apiSecret); | ||
params.signature = exports.api_sign_request(params, apiSecret, signature_algorithm, signature_version); | ||
params.api_key = apiKey; | ||
@@ -1569,5 +1558,3 @@ return params; | ||
if (breakpoint_settings.transformation) { | ||
breakpoint_settings.transformation = utils.generate_transformation_string( | ||
clone(breakpoint_settings.transformation) | ||
); | ||
breakpoint_settings.transformation = utils.generate_transformation_string(clone(breakpoint_settings.transformation)); | ||
} | ||
@@ -1582,7 +1569,5 @@ } | ||
if (isArray(params.representations)) { | ||
params.representations = JSON.stringify(params.representations.map( | ||
r => ({ | ||
transformation: utils.generate_transformation_string(r.transformation) | ||
}) | ||
)); | ||
params.representations = JSON.stringify(params.representations.map(r => ({ | ||
transformation: utils.generate_transformation_string(r.transformation) | ||
}))); | ||
} | ||
@@ -1612,5 +1597,3 @@ return params; | ||
function hashToQuery(hash) { | ||
return hashToParameters(hash).map( | ||
([key, value]) => `${querystring.escape(key)}=${querystring.escape(value)}` | ||
).join('&'); | ||
return hashToParameters(hash).map(([key, value]) => `${querystring.escape(key)}=${querystring.escape(value)}`).join('&'); | ||
} | ||
@@ -1758,1 +1741,25 @@ | ||
}); | ||
/** | ||
* Verifies an API response signature for a given public_id and version. | ||
* Always uses signature version 1 for backward compatibility, matching the Ruby SDK. | ||
* @param {string} public_id | ||
* @param {string|number} version | ||
* @param {string} signature | ||
* @returns {boolean} | ||
*/ | ||
function verify_api_response_signature(public_id, version, signature) { | ||
const api_secret = config().api_secret; | ||
const expected = exports.api_sign_request( | ||
{ | ||
public_id, | ||
version | ||
}, | ||
api_secret, | ||
null, | ||
1 | ||
); | ||
return signature === expected; | ||
} | ||
exports.verify_api_response_signature = verify_api_response_signature; |
@@ -5,3 +5,3 @@ { | ||
"description": "Cloudinary NPM for node.js integration", | ||
"version": "2.6.1", | ||
"version": "2.7.0", | ||
"homepage": "https://cloudinary.com", | ||
@@ -8,0 +8,0 @@ "license": "MIT", |
298046
0.66%6497
-0.02%