VYPR
High severity8.6OSV Advisory· Published Nov 10, 2025· Updated Apr 15, 2026

CVE-2025-12613

CVE-2025-12613

Description

Versions of the package cloudinary before 2.7.0 are vulnerable to Arbitrary Argument Injection due to improper parsing of parameter values containing an ampersand. An attacker can inject additional, unintended parameters. This could lead to a variety of malicious outcomes, such as bypassing security checks, altering data, or manipulating the application's behavior. Note: Following our established security policy, we attempted to contact the maintainer regarding this vulnerability, but haven't received a response.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
cloudinarynpm
< 2.7.02.7.0

Affected products

1

Patches

2
ec4b65f2b346

fix: prevent parameter injection via ampersand in parameter values (#709)

https://github.com/cloudinary/cloudinary_npmPatryk KoniorJun 18, 2025via ghsa
6 files changed · +389 156
  • lib/preloaded_file.js+1 6 modified
    @@ -24,12 +24,7 @@ class PreloadedFile {
       }
     
       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);
       }
     
       static split_format(identifier) {
    
  • lib/utils/index.js+78 71 modified
    @@ -544,9 +544,7 @@ function generate_transformation_string(options) {
       let base_transformations = toArray(consumeOption(options, "transformation", []));
       let named_transformation = [];
       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 {
         named_transformation = base_transformations.join(".");
         base_transformations = [];
    @@ -555,9 +553,7 @@ function generate_transformation_string(options) {
       if (isArray(effect)) {
         effect = effect.join(":");
       } else if (isObject(effect)) {
    -    effect = entries(effect).map(
    -      ([key, value]) => `${key}:${value}`
    -    );
    +    effect = entries(effect).map(([key, value]) => `${key}:${value}`);
       }
       let border = consumeOption(options, "border");
       if (isObject(border)) {
    @@ -634,9 +630,7 @@ function generate_transformation_string(options) {
         .map(([key, value]) => {
           delete options[key];
           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(',');
     
       let transformations = entries(params)
         .filter(([key, value]) => utils.present(value))
    @@ -649,8 +643,7 @@ function generate_transformation_string(options) {
       base_transformations.push(transformations);
       transformations = base_transformations;
       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;
     
         transformations.push(utils.generate_transformation_string(clone(responsive_width_transformation)));
       }
    @@ -745,27 +738,7 @@ function updateable_resource_params(options, params = {}) {
      * A list of keys used by the url() function.
      * @private
      */
    -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'];
     
     /**
      * Create a new object with only URL parameters
    @@ -930,9 +903,7 @@ function url(public_id, options = {}) {
           urlAnalytics
         };
     
    -    let analyticsOptions = getAnalyticsOptions(
    -      Object.assign({}, options, sdkVersions)
    -    );
    +    let analyticsOptions = getAnalyticsOptions(Object.assign({}, options, sdkVersions));
     
         let sdkAnalyticsSignature = getSDKAnalyticsSignature(analyticsOptions);
     
    @@ -1033,16 +1004,7 @@ function finalize_resource_type(resource_type, type, url_suffix, use_root_path,
     //    if cdn_domain is true uses a[1-5].cname for http.
     //    For https, uses the same naming scheme as 1 for shared distribution and as 2 for private distribution.
     
    -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;
       if (cloud_name.indexOf("/") === 0) {
         return '/res' + cloud_name;
    @@ -1112,13 +1074,42 @@ function signed_preloaded_image(result) {
       return `${result.resource_type}/upload/v${result.version}/${filter([result.public_id, result.format], utils.present).join(".")}#${result.signature}`;
     }
     
    -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');
     }
     
     /**
    @@ -1139,13 +1130,9 @@ function compute_hash(input, signature_algorithm, encoding) {
     
     function clear_blank(hash) {
       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;
     }
     
    @@ -1163,8 +1150,10 @@ function merge(hash1, hash2) {
     function sign_request(params, options = {}) {
       let apiKey = ensureOption(options, 'api_key');
       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;
       return params;
     }
    @@ -1556,9 +1545,7 @@ function generate_responsive_breakpoints_string(breakpoints) {
         let breakpoint_settings = breakpoints[j];
         if (breakpoint_settings != null) {
           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));
           }
         }
       }
    @@ -1568,11 +1555,9 @@ function generate_responsive_breakpoints_string(breakpoints) {
     function build_streaming_profiles_param(options = {}) {
       let params = pickOnlyExistingValues(options, "display_name", "representations");
       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)
    +    })));
       }
       return params;
     }
    @@ -1597,9 +1582,7 @@ function hashToParameters(hash) {
      * @return {string} A URI query string.
      */
     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('&');
     }
     
     /**
    @@ -1742,3 +1725,27 @@ Object.assign(module.exports, {
       keys: source => Object.keys(source),
       ensurePresenceOf
     });
    +
    +/**
    + * 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;
    
  • test/integration/api/admin/api_spec.js+2 2 modified
    @@ -1374,12 +1374,12 @@ describe("api", function () {
           const secondAssetVersion = getVersionsResp.versions[1].version_id;
     
           // Restore first version, ensure it's equal to the upload size
    -      await wait(1000)();
    +      await wait(2000)();
           const firstVerRestore = await API_V2.restore([PUBLIC_ID_BACKUP_1], {versions: [firstAssetVersion]});
           expect(firstVerRestore[PUBLIC_ID_BACKUP_1].bytes).to.eql(firstUpload.bytes);
     
           // Restore second version, ensure it's equal to the upload size
    -      await wait(1000)();
    +      await wait(2000)();
           const secondVerRestore = await API_V2.restore([PUBLIC_ID_BACKUP_1], {versions: [secondAssetVersion]});
           expect(secondVerRestore[PUBLIC_ID_BACKUP_1].bytes).to.eql(secondUpload.bytes);
     
    
  • test/integration/api/uploader/uploader_spec.js+63 28 modified
    @@ -31,7 +31,7 @@ const METADATA_SAMPLE_DATA_ENCODED = "metadata_color=red|metadata_shape=dodecahe
     const createTestConfig = require('../../../testUtils/createTestConfig');
     
     const testConstants = require('../../../testUtils/testConstants');
    -const {shouldTestFeature, DYNAMIC_FOLDERS} = require("../../../spechelper");
    +const { shouldTestFeature, DYNAMIC_FOLDERS } = require("../../../spechelper");
     const UPLOADER_V2 = cloudinary.v2.uploader;
     
     const {
    @@ -136,11 +136,23 @@ describe("uploader", function () {
           tags: UPLOAD_TAGS
         });
       });
    +  it('should allow uploading with parameters containing &', function () {
    +    const publicId = `ampersand-test-${Date.now()}`;
    +    return cloudinary.v2.uploader.upload('https://cloudinary.com/images/old_logo.png', {
    +      notification_url: 'https://example.com?exampleparam1=aaa&exampleparam2=bbb',
    +      public_id: publicId
    +    }).then((result) => {
    +      expect(result).to.have.property('public_id');
    +      expect(result.public_id).to.equal(publicId);
    +    }).catch((error) => {
    +      expect(error).to.be(null);
    +    });
    +  });
       it('should allow upload with url safe base64 in overlay', function () {
         const overlayUrl = 'https://res.cloudinary.com/demo/image/upload/logos/cloudinary_full_logo_white_small.png';
    -    const baseImageUrl ='https://cloudinary.com/images/old_logo.png';
    +    const baseImageUrl = 'https://cloudinary.com/images/old_logo.png';
     
    -    const options = {transformation: {overlay: { url: overlayUrl }}};
    +    const options = { transformation: { overlay: { url: overlayUrl } } };
         return cloudinary.v2.uploader.upload(baseImageUrl, options)
           .then((result) => {
             expect(result).to.have.key("created_at");
    @@ -201,7 +213,7 @@ describe("uploader", function () {
         it('should include tags in rename response if requested explicitly', async () => {
           const uploadResult = await cloudinary.v2.uploader.upload(IMAGE_FILE, { context: 'alt=Example|class=Example', tags: ['test-tag'] });
     
    -      const renameResult = await cloudinary.v2.uploader.rename(uploadResult.public_id, `${uploadResult.public_id}-renamed`, {tags: true, context: true});
    +      const renameResult = await cloudinary.v2.uploader.rename(uploadResult.public_id, `${uploadResult.public_id}-renamed`, { tags: true, context: true });
     
           expect(renameResult).to.have.property('tags');
           expect(renameResult).to.have.property('context');
    @@ -941,7 +953,7 @@ describe("uploader", function () {
       it("should reject with promise rejection if disable_promises: false", function (done) {
         const spy = sinon.spy();
     
    -    cloudinary.v2.uploader.upload_large(EMPTY_IMAGE, { disable_promises: false }, () => {});
    +    cloudinary.v2.uploader.upload_large(EMPTY_IMAGE, { disable_promises: false }, () => { });
     
         function unhandledRejection() {
           spy();
    @@ -959,7 +971,7 @@ describe("uploader", function () {
       it("should reject with promise rejection by default", function (done) {
         const spy = sinon.spy();
     
    -    cloudinary.v2.uploader.upload_large(EMPTY_IMAGE, () => {});
    +    cloudinary.v2.uploader.upload_large(EMPTY_IMAGE, () => { });
     
         function unhandledRejection() {
           spy();
    @@ -977,7 +989,7 @@ describe("uploader", function () {
       it("should reject without promise rejection if disable_promises: true", function (done) {
         const spy = sinon.spy();
     
    -    cloudinary.v2.uploader.upload_large(EMPTY_IMAGE, { disable_promises: true }, () => {});
    +    cloudinary.v2.uploader.upload_large(EMPTY_IMAGE, { disable_promises: true }, () => { });
     
         function unhandledRejection() {
           spy();
    @@ -1035,7 +1047,7 @@ describe("uploader", function () {
         this.timeout(TIMEOUT.LONG);
         expect(cloudinary.v2.uploader.upload_stream).withArgs({
           agent: new http.Agent()
    -    }, function (error, result) {}).to.throwError();
    +    }, function (error, result) { }).to.throwError();
       });
       it("should successfully override https agent", function () {
         var file_reader, upload;
    @@ -1285,7 +1297,7 @@ describe("uploader", function () {
             this.skip();
           }
           // Upload an image and request ocr details in the response
    -      const result = await UPLOADER_V2.upload(IMAGE_FILE, {ocr: ocrType, tags: [TEST_TAG]});
    +      const result = await UPLOADER_V2.upload(IMAGE_FILE, { ocr: ocrType, tags: [TEST_TAG] });
     
           // Ensure result includes properly structured ocr details
           expect(result).not.to.be.empty();
    @@ -1325,11 +1337,11 @@ describe("uploader", function () {
             external_id: METADATA_FIELD_UNIQUE_EXTERNAL_ID,
             label: METADATA_FIELD_UNIQUE_EXTERNAL_ID,
             type: "string"
    -      }).finally(function () {});
    +      }).finally(function () { });
         });
         after(function () {
           return cloudinary.v2.api.delete_metadata_field(METADATA_FIELD_UNIQUE_EXTERNAL_ID)
    -        .finally(function () {});
    +        .finally(function () { });
         });
         it("should be set when calling upload with metadata", function () {
           return uploadImage({
    @@ -1501,8 +1513,8 @@ describe("uploader", function () {
         before(async function () {
           // Upload images to be used by sprite and multi
           const uploads = await Promise.all([
    -        uploadImage({tags: [SPRITE_TEST_TAG, ...UPLOAD_TAGS]}),
    -        uploadImage({tags: [SPRITE_TEST_TAG, ...UPLOAD_TAGS]})
    +        uploadImage({ tags: [SPRITE_TEST_TAG, ...UPLOAD_TAGS] }),
    +        uploadImage({ tags: [SPRITE_TEST_TAG, ...UPLOAD_TAGS] })
           ]);
           uploaded_url_1 = uploads[0].url;
           uploaded_url_2 = uploads[1].url;
    @@ -1515,23 +1527,23 @@ describe("uploader", function () {
         });
         it("should generate a sprite by tag with raw transformation", async function () {
           const result = await UPLOADER_V2.generate_sprite(SPRITE_TEST_TAG, {
    -        transformation: {raw_transformation: 'w_100'}
    +        transformation: { raw_transformation: 'w_100' }
           });
           expect(result).to.beASprite();
           expect(result.css_url).to.contain('w_100');
         });
         it("should generate a sprite by tag with transformation params", async function () {
    -      const result = await UPLOADER_V2.generate_sprite(SPRITE_TEST_TAG, {width: 100, format: 'jpg'});
    +      const result = await UPLOADER_V2.generate_sprite(SPRITE_TEST_TAG, { width: 100, format: 'jpg' });
           expect(result).to.beASprite('jpg');
           expect(result.css_url).to.contain('f_jpg,w_100');
         });
         it("should generate a sprite by URLs array", async function () {
    -      const result = await UPLOADER_V2.generate_sprite({'urls': [uploaded_url_1, uploaded_url_2]});
    +      const result = await UPLOADER_V2.generate_sprite({ 'urls': [uploaded_url_1, uploaded_url_2] });
           expect(result).to.beASprite();
           expect(Object.entries(result.image_infos).length).to.eql(2);
         });
         it("should generate an url to download a sprite by URLs array", function () {
    -      const url = UPLOADER_V2.download_generated_sprite({'urls': [SAMPLE_IMAGE_URL_1, SAMPLE_IMAGE_URL_2]});
    +      const url = UPLOADER_V2.download_generated_sprite({ 'urls': [SAMPLE_IMAGE_URL_1, SAMPLE_IMAGE_URL_2] });
           expect(url).to.beASignedDownloadUrl("image/sprite", { urls: [SAMPLE_IMAGE_URL_1, SAMPLE_IMAGE_URL_2] });
         });
         it("should generate an url to download a sprite by tag", async function () {
    @@ -1547,34 +1559,34 @@ describe("uploader", function () {
         before(async function () {
           // Upload images to be used by sprite and multi
           const uploads = await Promise.all([
    -        uploadImage({tags: [MULTI_TEST_TAG, ...UPLOAD_TAGS]}),
    -        uploadImage({tags: [MULTI_TEST_TAG, ...UPLOAD_TAGS]})
    +        uploadImage({ tags: [MULTI_TEST_TAG, ...UPLOAD_TAGS] }),
    +        uploadImage({ tags: [MULTI_TEST_TAG, ...UPLOAD_TAGS] })
           ]);
           uploaded_url_1 = uploads[0].url;
           uploaded_url_2 = uploads[1].url;
         });
     
         it("should create a pdf by tag", async function () {
    -      const result = await UPLOADER_V2.multi(MULTI_TEST_TAG, {format: "pdf"});
    +      const result = await UPLOADER_V2.multi(MULTI_TEST_TAG, { format: "pdf" });
           expect(result).to.beAMulti();
           expect(result.url).to.match(new RegExp(`\.pdf$`));
         });
         it("should create a gif with a transformation by tag", async function () {
           const options = { width: 0.5, crop: "crop" };
           const transformation = cloudinary.utils.generate_transformation_string(Object.assign({}, options));
    -      const result = await UPLOADER_V2.multi(MULTI_TEST_TAG, {transformation: options });
    +      const result = await UPLOADER_V2.multi(MULTI_TEST_TAG, { transformation: options });
           expect(result).to.beAMulti();
           expect(result.url).to.match(new RegExp(`/image/multi/${transformation}/.*\.gif$`));
         });
         it("should generate a gif with a transformation by URLs array", async function () {
           const options = { width: 0.5, crop: "crop" };
           const transformation = cloudinary.utils.generate_transformation_string(Object.assign({}, options));
    -      const result = await UPLOADER_V2.multi({ urls: [uploaded_url_1, uploaded_url_2], transformation: options});
    +      const result = await UPLOADER_V2.multi({ urls: [uploaded_url_1, uploaded_url_2], transformation: options });
           expect(result).to.beAMulti();
           expect(result.url).to.match(new RegExp(`/image/multi/${transformation}/.*\.gif$`));
         });
         it("should generate a download URL for a gif by URLs array", function () {
    -      const url = UPLOADER_V2.download_multi({ urls: [SAMPLE_IMAGE_URL_1, SAMPLE_IMAGE_URL_2]});
    +      const url = UPLOADER_V2.download_multi({ urls: [SAMPLE_IMAGE_URL_1, SAMPLE_IMAGE_URL_2] });
           expect(url).to.beASignedDownloadUrl("image/multi", { urls: [SAMPLE_IMAGE_URL_1, SAMPLE_IMAGE_URL_2] });
         });
         it("should generate a download URL for a gif by tag", function () {
    @@ -1586,26 +1598,49 @@ describe("uploader", function () {
         const mocked = helper.mockTest();
         const proxy = "https://myuser:mypass@example.com"
         it("should support proxy for upload calls", function () {
    -      cloudinary.config({api_proxy: proxy});
    -      UPLOADER_V2.upload(IMAGE_FILE, {"tags": [TEST_TAG]});
    +      cloudinary.config({ api_proxy: proxy });
    +      UPLOADER_V2.upload(IMAGE_FILE, { "tags": [TEST_TAG] });
           sinon.assert.calledWith(mocked.request, sinon.match(
             arg => arg.agent instanceof https.Agent
           ));
         });
         it("should prioritize custom agent", function () {
    -      cloudinary.config({api_proxy: proxy});
    +      cloudinary.config({ api_proxy: proxy });
           const custom_agent = https.Agent()
    -      UPLOADER_V2.upload(IMAGE_FILE, {"tags": [TEST_TAG], agent: custom_agent});
    +      UPLOADER_V2.upload(IMAGE_FILE, { "tags": [TEST_TAG], agent: custom_agent });
           sinon.assert.calledWith(mocked.request, sinon.match(
             arg => arg.agent === custom_agent
           ));
         });
         it("should support api_proxy as options key", function () {
           cloudinary.config({});
    -      UPLOADER_V2.upload(IMAGE_FILE, {"tags": [TEST_TAG], api_proxy: proxy});
    +      UPLOADER_V2.upload(IMAGE_FILE, { "tags": [TEST_TAG], api_proxy: proxy });
           sinon.assert.calledWith(mocked.request, sinon.match(
             arg => arg.agent instanceof https.Agent
           ));
         });
       })
    +  describe("signature_version parameter support", function () {
    +    it("should use signature_version from config when not specified", function () {
    +      const original_signature_version = cloudinary.config().signature_version;
    +      cloudinary.config({ signature_version: 1 });
    +      let upload_result;
    +      return uploadImage()
    +        .then(function (result) {
    +          upload_result = result;
    +          const public_id = result.public_id;
    +          const version = result.version;
    +          const expected_signature_v1 = cloudinary.utils.api_sign_request(
    +            { public_id: public_id, version: version },
    +            cloudinary.config().api_secret,
    +            null,
    +            1
    +          );
    +          expect(result.signature).to.eql(expected_signature_v1);
    +        })
    +        .finally(function () {
    +          cloudinary.config({ signature_version: original_signature_version });
    +        });
    +    });
    +  });
     });
    
  • test/unit/cloudinary_spec.js+167 49 modified
    @@ -1,6 +1,9 @@
     const cloudinary = require("../../cloudinary");
     const createTestConfig = require('../testUtils/createTestConfig');
     
    +const API_SIGN_REQUEST_TEST_SECRET = "hdcixPpR2iKERPwqvH6sHdK9cyac";
    +const API_SIGN_REQUEST_CLOUD_NAME = "dn6ot3ged";
    +
     describe("cloudinary", function () {
       beforeEach(function () {
         cloudinary.config(createTestConfig({
    @@ -203,9 +206,9 @@ describe("cloudinary", function () {
           })).to.eql(`${upload_path}/g_center,p_a,q_auto:good,r_3,x_1,y_2/test`);
         });
       });
    -  describe(":radius", function() {
    +  describe(":radius", function () {
         const upload_path = 'https://res.cloudinary.com/test123/image/upload';
    -    it("should support a single value", function() {
    +    it("should support a single value", function () {
           expect(cloudinary.utils.url("test", {
             radius: 10
           })).to.eql(`${upload_path}/r_10/test`);
    @@ -217,7 +220,7 @@ describe("cloudinary", function () {
             radius: '$v'
           })).to.eql(`${upload_path}/$v_10,r_$v/test`);
         });
    -    it("should support an array of values", function() {
    +    it("should support an array of values", function () {
           expect(cloudinary.utils.url("test", {
             radius: [10, 20, 30]
           })).to.eql(`${upload_path}/r_10:20:30/test`);
    @@ -230,7 +233,7 @@ describe("cloudinary", function () {
             radius: [10, 20, '$v', 40]
           })).to.eql(`${upload_path}/$v_10,r_10:20:$v:40/test`);
         })
    -    it("should support colon separated values", function() {
    +    it("should support colon separated values", function () {
           expect(cloudinary.utils.url("test", {
             radius: "10:20"
           })).to.eql(`${upload_path}/r_10:20/test`);
    @@ -240,7 +243,7 @@ describe("cloudinary", function () {
           })).to.eql(`${upload_path}/$v_10,r_10:20:$v:40/test`);
         })
       })
    -  it("should support named transformation", function() {
    +  it("should support named transformation", function () {
         var options, result;
         options = {
           transformation: "blip"
    @@ -464,38 +467,6 @@ describe("cloudinary", function () {
           expect(result).to.eql(`https://res.cloudinary.com/test123/image/upload/h_100,${short}_text:hello,w_100/test`);
         });
       });
    -  it("should correctly sign api requests", function () {
    -    expect(cloudinary.utils.api_sign_request({
    -      hello: null,
    -      goodbye: 12,
    -      world: "problem",
    -      undef: void 0
    -    }, "1234")).to.eql("f05cfe85cee78e7e997b3c7da47ba212dcbf1ea5");
    -  });
    -  it("should correctly sign api requests with signature algorithm SHA1", function () {
    -    cloudinary.config({ signature_algorithm: 'sha1' });
    -    expect(cloudinary.utils.api_sign_request({
    -      username: "user@cloudinary.com",
    -      timestamp: 1568810420,
    -      cloud_name: "dn6ot3ged"
    -    }, "hdcixPpR2iKERPwqvH6sHdK9cyac")).to.eql("14c00ba6d0dfdedbc86b316847d95b9e6cd46d94");
    -  });
    -  it("should correctly sign api requests with signature algorithm SHA1 as default", function () {
    -    cloudinary.config({ signature_algorithm: null });
    -    expect(cloudinary.utils.api_sign_request({
    -      username: "user@cloudinary.com",
    -      timestamp: 1568810420,
    -      cloud_name: "dn6ot3ged"
    -    }, "hdcixPpR2iKERPwqvH6sHdK9cyac")).to.eql("14c00ba6d0dfdedbc86b316847d95b9e6cd46d94");
    -  });
    -  it("should correctly sign api requests with signature algorithm SHA256", function () {
    -    cloudinary.config({ signature_algorithm: 'sha256' });
    -    expect(cloudinary.utils.api_sign_request({
    -      username: "user@cloudinary.com",
    -      timestamp: 1568810420,
    -      cloud_name: "dn6ot3ged"
    -    }, "hdcixPpR2iKERPwqvH6sHdK9cyac")).to.eql("45ddaa4fa01f0c2826f32f669d2e4514faf275fe6df053f1a150e7beae58a3bd");
    -  });
       it("should correctly build signed preloaded image", function () {
         expect(cloudinary.utils.signed_preloaded_image({
           resource_type: "image",
    @@ -866,19 +837,166 @@ describe("cloudinary", function () {
         expect(result).to.eql('https://res.cloudinary.com/test123/image/upload/s--2hbrSMPO--/sample.jpg');
       });
     
    -  it("should not affect user variable names containing predefined names", function() {
    -    const options = { transformation: [
    -      {
    -        $mywidth: "100",
    -        $aheight: 300
    -      },
    -      {
    -        width: "3 + $mywidth * 3 + 4 / 2 * initialWidth * $mywidth",
    -        height: "3 * initialHeight + $aheight",
    -        crop: 'scale'
    -      }
    -    ]};
    +  it("should not affect user variable names containing predefined names", function () {
    +    const options = {
    +      transformation: [
    +        {
    +          $mywidth: "100",
    +          $aheight: 300
    +        },
    +        {
    +          width: "3 + $mywidth * 3 + 4 / 2 * initialWidth * $mywidth",
    +          height: "3 * initialHeight + $aheight",
    +          crop: 'scale'
    +        }
    +      ]
    +    };
         const result = cloudinary.utils.url("sample", options);
         expect(result).to.contain("$aheight_300,$mywidth_100/c_scale,h_3_mul_ih_add_$aheight,w_3_add_$mywidth_mul_3_add_4_div_2_mul_iw_mul_$mywidth");
       });
     });
    +
    +describe("api_sign_request", function () {
    +  it("should sign an API request using SHA1 by default", function () {
    +    const signature = cloudinary.utils.api_sign_request({
    +      cloud_name: API_SIGN_REQUEST_CLOUD_NAME,
    +      timestamp: 1568810420,
    +      username: "user@cloudinary.com"
    +    }, API_SIGN_REQUEST_TEST_SECRET);
    +    expect(signature).to.eql("14c00ba6d0dfdedbc86b316847d95b9e6cd46d94");
    +  });
    +
    +  it("should sign an API request using SHA256", function () {
    +    cloudinary.config({ signature_algorithm: 'sha256' });
    +    const signature = cloudinary.utils.api_sign_request({
    +      cloud_name: API_SIGN_REQUEST_CLOUD_NAME,
    +      timestamp: 1568810420,
    +      username: "user@cloudinary.com"
    +    }, API_SIGN_REQUEST_TEST_SECRET);
    +    expect(signature).to.eql("45ddaa4fa01f0c2826f32f669d2e4514faf275fe6df053f1a150e7beae58a3bd");
    +    cloudinary.config(true);
    +  });
    +
    +  it("should sign an API request using SHA256 via parameter", function () {
    +    const signature = cloudinary.utils.api_sign_request({
    +      cloud_name: API_SIGN_REQUEST_CLOUD_NAME,
    +      timestamp: 1568810420,
    +      username: "user@cloudinary.com"
    +    }, API_SIGN_REQUEST_TEST_SECRET, "sha256");
    +    expect(signature).to.eql("45ddaa4fa01f0c2826f32f669d2e4514faf275fe6df053f1a150e7beae58a3bd");
    +  });
    +
    +  it("should raise when unsupported algorithm is passed", function () {
    +    const signature_algorithm = "unsupported_algorithm";
    +    expect(() => {
    +      cloudinary.utils.api_sign_request({
    +        cloud_name: API_SIGN_REQUEST_CLOUD_NAME,
    +        timestamp: 1568810420,
    +        username: "user@cloudinary.com"
    +      }, API_SIGN_REQUEST_TEST_SECRET, signature_algorithm);
    +    }).to.throwException();
    +  });
    +
    +  it("should prevent parameter smuggling via & characters in parameter values with signature version 2", function () {
    +    const params_with_ampersand = {
    +      cloud_name: API_SIGN_REQUEST_CLOUD_NAME,
    +      timestamp: 1568810420,
    +      notification_url: "https://fake.com/callback?a=1&tags=hello,world"
    +    };
    +    const signature_v1_with_ampersand = cloudinary.utils.api_sign_request(params_with_ampersand, API_SIGN_REQUEST_TEST_SECRET, null, 1);
    +    const signature_v2_with_ampersand = cloudinary.utils.api_sign_request(params_with_ampersand, API_SIGN_REQUEST_TEST_SECRET, null, 2);
    +
    +    const params_smuggled = {
    +      cloud_name: API_SIGN_REQUEST_CLOUD_NAME,
    +      timestamp: 1568810420,
    +      notification_url: "https://fake.com/callback?a=1",
    +      tags: "hello,world"
    +    };
    +    const signature_v1_smuggled = cloudinary.utils.api_sign_request(params_smuggled, API_SIGN_REQUEST_TEST_SECRET, null, 1);
    +    const signature_v2_smuggled = cloudinary.utils.api_sign_request(params_smuggled, API_SIGN_REQUEST_TEST_SECRET, null, 2);
    +
    +    expect(signature_v1_with_ampersand).to.eql(signature_v1_smuggled);
    +    expect(signature_v2_with_ampersand).to.not.eql(signature_v2_smuggled);
    +    expect(signature_v2_with_ampersand).to.eql("4fdf465dd89451cc1ed8ec5b3e314e8a51695704");
    +    expect(signature_v2_smuggled).to.eql("7b4e3a539ff1fa6e6700c41b3a2ee77586a025f9");
    +  });
    +
    +  it("should use signature version 1 (without parameter encoding) for backward compatibility", function () {
    +    const public_id_with_ampersand = 'tests/logo&version=2';
    +    const test_version = 123456;
    +    const SIGNATURE_VERIFICATION_API_SECRET = "testsecret";
    +    const expected_signature_v1 = cloudinary.utils.api_sign_request(
    +      { public_id: public_id_with_ampersand, version: test_version },
    +      SIGNATURE_VERIFICATION_API_SECRET,
    +      null,
    +      1
    +    );
    +    const expected_signature_v2 = cloudinary.utils.api_sign_request(
    +      { public_id: public_id_with_ampersand, version: test_version },
    +      SIGNATURE_VERIFICATION_API_SECRET,
    +      null,
    +      2
    +    );
    +    expect(expected_signature_v1).to.not.eql(expected_signature_v2);
    +    expect(cloudinary.utils.verify_api_response_signature(public_id_with_ampersand, test_version, expected_signature_v1)).to.be.true;
    +    expect(cloudinary.utils.verify_api_response_signature(public_id_with_ampersand, test_version, expected_signature_v2)).to.be.false;
    +  });
    +});
    +
    +describe("Response signature verification fixes", function () {
    +  const public_id = 'tests/logo.png';
    +  const test_version = 1234;
    +  const test_api_secret = "testsecret";
    +
    +  describe("api_sign_request signature_version parameter support", function () {
    +    it("should support signature_version parameter in api_sign_request", function () {
    +      const params = { public_id: public_id, version: test_version };
    +      const signature_v1 = cloudinary.utils.api_sign_request(params, test_api_secret, null, 1);
    +      const signature_v2 = cloudinary.utils.api_sign_request(params, test_api_secret, null, 2);
    +      expect(signature_v1).to.be.a('string');
    +      expect(signature_v2).to.be.a('string');
    +      expect(signature_v1).to.eql(signature_v2); // No & in values, so should be the same
    +    });
    +
    +    it("should use default signature_version from config", function () {
    +      const original_signature_version = cloudinary.config().signature_version;
    +      cloudinary.config({ signature_version: 2 });
    +      const params = { public_id: public_id, version: test_version };
    +      const signature_with_nil = cloudinary.utils.api_sign_request(params, test_api_secret, null, null);
    +      const signature_with_v2 = cloudinary.utils.api_sign_request(params, test_api_secret, null, 2);
    +      expect(signature_with_nil).to.eql(signature_with_v2);
    +      cloudinary.config({ signature_version: original_signature_version });
    +    });
    +
    +    it("should default to version 2 when no config is set", function () {
    +      const original_signature_version = cloudinary.config().signature_version;
    +      cloudinary.config({ signature_version: null });
    +      const params = { public_id: public_id, version: test_version };
    +      const signature_with_nil = cloudinary.utils.api_sign_request(params, test_api_secret, null, null);
    +      const signature_with_v2 = cloudinary.utils.api_sign_request(params, test_api_secret, null, 2);
    +      expect(signature_with_nil).to.eql(signature_with_v2);
    +      cloudinary.config({ signature_version: original_signature_version });
    +    });
    +  });
    +});
    +
    +describe("verify_api_response_signature", function () {
    +  const public_id = 'tests/logo.png';
    +  const version = 1234;
    +  const test_api_secret = "testsecret";
    +  before(function () {
    +    cloudinary.config({ api_secret: test_api_secret });
    +  });
    +  it("should return true for a valid signature (number version)", function () {
    +    const signature = cloudinary.utils.api_sign_request({ public_id, version }, test_api_secret, null, 1);
    +    expect(cloudinary.utils.verify_api_response_signature(public_id, version, signature)).to.be(true);
    +  });
    +  it("should return true for a valid signature (string version)", function () {
    +    const version_str = version.toString();
    +    const signature = cloudinary.utils.api_sign_request({ public_id, version: version_str }, test_api_secret, null, 1);
    +    expect(cloudinary.utils.verify_api_response_signature(public_id, version_str, signature)).to.be(true);
    +  });
    +  it("should return false for an invalid signature", function () {
    +    expect(cloudinary.utils.verify_api_response_signature(public_id, version, "invalidsignature")).to.be(false);
    +  });
    +});
    
  • test/unit/preloaded_file_spec.js+78 0 added
    @@ -0,0 +1,78 @@
    +const expect = require('expect.js');
    +const cloudinary = require('../../cloudinary');
    +const PreloadedFile = require('../../lib/preloaded_file');
    +
    +const TEST_API_SECRET = "X7qLTrsES31MzxxkxPPA-pAGGfU";
    +
    +describe('PreloadedFile', function () {
    +  before(function () {
    +    cloudinary.config({api_secret: TEST_API_SECRET});
    +  });
    +
    +  describe('folder support', function () {
    +    it('should allow to use folders in PreloadedFile', function () {
    +      const signature = cloudinary.utils.api_sign_request({
    +        public_id: 'folder/file',
    +        version: '1234'
    +      }, TEST_API_SECRET);
    +      const preloaded = new PreloadedFile(`image/upload/v1234/folder/file.jpg#${signature}`);
    +      expect(preloaded.is_valid()).to.be(true);
    +      expect(preloaded.filename).to.eql('folder/file.jpg');
    +      expect(preloaded.version).to.eql('1234');
    +      expect(preloaded.public_id).to.eql('folder/file');
    +      expect(preloaded.signature).to.eql(signature);
    +      expect(preloaded.resource_type).to.eql('image');
    +      expect(preloaded.type).to.eql('upload');
    +      expect(preloaded.format).to.eql('jpg');
    +    });
    +  });
    +
    +  describe('signature verification', function () {
    +    const public_id = 'tests/logo.png';
    +    const test_version = 1234;
    +
    +    it('should correctly verify signature with proper parameter order', function () {
    +      const filename_with_format = public_id;
    +      const public_id_without_format = 'tests/logo';
    +      const version_string = test_version.toString();
    +      const expected_signature = cloudinary.utils.api_sign_request(
    +        {
    +          public_id: public_id_without_format,
    +          version: version_string
    +        },
    +        TEST_API_SECRET,
    +        null,
    +        1
    +      );
    +      const preloaded_string = `image/upload/v${version_string}/${filename_with_format}#${expected_signature}`;
    +      const preloaded_file = new PreloadedFile(preloaded_string);
    +      expect(preloaded_file.is_valid()).to.be(true);
    +    });
    +
    +    it('should fail verification with incorrect signature', function () {
    +      const wrong_signature = 'wrongsignature';
    +      const preloaded_string = `image/upload/v${test_version}/${public_id}#${wrong_signature}`;
    +      const preloaded_file = new PreloadedFile(preloaded_string);
    +      expect(preloaded_file.is_valid()).to.be(false);
    +    });
    +
    +    it('should handle raw resource type correctly', function () {
    +      const raw_filename = 'document.pdf';
    +      const public_id_without_format = 'document';
    +      const version_string = test_version.toString();
    +      const raw_signature = cloudinary.utils.api_sign_request(
    +        {
    +          public_id: public_id_without_format,
    +          version: version_string
    +        },
    +        TEST_API_SECRET,
    +        null,
    +        1
    +      );
    +      const preloaded_string = `raw/upload/v${version_string}/${raw_filename}#${raw_signature}`;
    +      const preloaded_file = new PreloadedFile(preloaded_string);
    +      expect(preloaded_file.is_valid()).to.be(true);
    +      expect(preloaded_file.resource_type).to.eql('raw');
    +    });
    +  });
    +});
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.