@@ -544,9 +544,7 @@ function generate_transformation_string(options) {
544544 let base_transformations = toArray ( consumeOption ( options , "transformation" , [ ] ) ) ;
545545 let named_transformation = [ ] ;
546546 if ( base_transformations . some ( isObject ) ) {
547- base_transformations = base_transformations . map ( tr => utils . generate_transformation_string (
548- isObject ( tr ) ? clone ( tr ) : { transformation : tr }
549- ) ) ;
547+ base_transformations = base_transformations . map ( tr => utils . generate_transformation_string ( isObject ( tr ) ? clone ( tr ) : { transformation : tr } ) ) ;
550548 } else {
551549 named_transformation = base_transformations . join ( "." ) ;
552550 base_transformations = [ ] ;
@@ -555,9 +553,7 @@ function generate_transformation_string(options) {
555553 if ( isArray ( effect ) ) {
556554 effect = effect . join ( ":" ) ;
557555 } else if ( isObject ( effect ) ) {
558- effect = entries ( effect ) . map (
559- ( [ key , value ] ) => `${ key } :${ value } `
560- ) ;
556+ effect = entries ( effect ) . map ( ( [ key , value ] ) => `${ key } :${ value } ` ) ;
561557 }
562558 let border = consumeOption ( options , "border" ) ;
563559 if ( isObject ( border ) ) {
@@ -634,9 +630,7 @@ function generate_transformation_string(options) {
634630 . map ( ( [ key , value ] ) => {
635631 delete options [ key ] ;
636632 return `${ key } _${ normalize_expression ( value ) } ` ;
637- } ) . sort ( ) . concat (
638- variablesParam . map ( ( [ name , value ] ) => `${ name } _${ normalize_expression ( value ) } ` )
639- ) . join ( ',' ) ;
633+ } ) . sort ( ) . concat ( variablesParam . map ( ( [ name , value ] ) => `${ name } _${ normalize_expression ( value ) } ` ) ) . join ( ',' ) ;
640634
641635 let transformations = entries ( params )
642636 . filter ( ( [ key , value ] ) => utils . present ( value ) )
@@ -649,8 +643,7 @@ function generate_transformation_string(options) {
649643 base_transformations . push ( transformations ) ;
650644 transformations = base_transformations ;
651645 if ( responsive_width ) {
652- let responsive_width_transformation = config ( ) . responsive_width_transformation
653- || DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION ;
646+ let responsive_width_transformation = config ( ) . responsive_width_transformation || DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION ;
654647
655648 transformations . push ( utils . generate_transformation_string ( clone ( responsive_width_transformation ) ) ) ;
656649 }
@@ -745,27 +738,7 @@ function updateable_resource_params(options, params = {}) {
745738 * A list of keys used by the url() function.
746739 * @private
747740 */
748- const URL_KEYS = [
749- 'api_secret' ,
750- 'auth_token' ,
751- 'cdn_subdomain' ,
752- 'cloud_name' ,
753- 'cname' ,
754- 'format' ,
755- 'long_url_signature' ,
756- 'private_cdn' ,
757- 'resource_type' ,
758- 'secure' ,
759- 'secure_cdn_subdomain' ,
760- 'secure_distribution' ,
761- 'shorten' ,
762- 'sign_url' ,
763- 'ssl_detected' ,
764- 'type' ,
765- 'url_suffix' ,
766- 'use_root_path' ,
767- 'version'
768- ] ;
741+ 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' ] ;
769742
770743/**
771744 * Create a new object with only URL parameters
@@ -930,9 +903,7 @@ function url(public_id, options = {}) {
930903 urlAnalytics
931904 } ;
932905
933- let analyticsOptions = getAnalyticsOptions (
934- Object . assign ( { } , options , sdkVersions )
935- ) ;
906+ let analyticsOptions = getAnalyticsOptions ( Object . assign ( { } , options , sdkVersions ) ) ;
936907
937908 let sdkAnalyticsSignature = getSDKAnalyticsSignature ( analyticsOptions ) ;
938909
@@ -1033,16 +1004,7 @@ function finalize_resource_type(resource_type, type, url_suffix, use_root_path,
10331004// if cdn_domain is true uses a[1-5].cname for http.
10341005// For https, uses the same naming scheme as 1 for shared distribution and as 2 for private distribution.
10351006
1036- function unsigned_url_prefix (
1037- source ,
1038- cloud_name ,
1039- private_cdn ,
1040- cdn_subdomain ,
1041- secure_cdn_subdomain ,
1042- cname ,
1043- secure ,
1044- secure_distribution
1045- ) {
1007+ function unsigned_url_prefix ( source , cloud_name , private_cdn , cdn_subdomain , secure_cdn_subdomain , cname , secure , secure_distribution ) {
10461008 let prefix ;
10471009 if ( cloud_name . indexOf ( "/" ) === 0 ) {
10481010 return '/res' + cloud_name ;
@@ -1112,13 +1074,42 @@ function signed_preloaded_image(result) {
11121074 return `${ result . resource_type } /upload/v${ result . version } /${ filter ( [ result . public_id , result . format ] , utils . present ) . join ( "." ) } #${ result . signature } ` ;
11131075}
11141076
1115- function api_sign_request ( params_to_sign , api_secret ) {
1116- let to_sign = entries ( params_to_sign ) . filter (
1117- ( [ k , v ] ) => utils . present ( v )
1118- ) . map (
1119- ( [ k , v ] ) => `${ k } =${ toArray ( v ) . join ( "," ) } `
1120- ) . sort ( ) . join ( "&" ) ;
1121- return compute_hash ( to_sign + api_secret , config ( ) . signature_algorithm || DEFAULT_SIGNATURE_ALGORITHM , 'hex' ) ;
1077+ // Encodes a parameter for safe inclusion in URL query strings (only replaces & with %26)
1078+ function encode_param ( value ) {
1079+ return String ( value ) . replace ( / & / g, '%26' ) ;
1080+ }
1081+
1082+ // Generates a string to be signed for API requests
1083+ function api_string_to_sign ( params_to_sign , signature_version = 2 ) {
1084+ let params = entries ( params_to_sign )
1085+ . map ( ( [ k , v ] ) => [ String ( k ) , Array . isArray ( v ) ? v . join ( "," ) : v ] )
1086+ . filter ( ( [ k , v ] ) => v !== null && v !== undefined && v !== "" ) ;
1087+ params . sort ( ( a , b ) => a [ 0 ] . localeCompare ( b [ 0 ] ) ) ;
1088+ let paramStrings = params . map ( ( [ k , v ] ) => {
1089+ const paramString = `${ k } =${ v } ` ;
1090+ return signature_version >= 2 ? encode_param ( paramString ) : paramString ;
1091+ } ) ;
1092+ return paramStrings . join ( "&" ) ;
1093+ }
1094+
1095+ /**
1096+ * Signs API request parameters
1097+ * @param {Object } params_to_sign Parameters to sign
1098+ * @param {string } api_secret API secret
1099+ * @param {string|undefined|null } signature_algorithm Hash algorithm to use ('sha1' or 'sha256')
1100+ * @param {number|undefined|null } signature_version Version of signature algorithm to use:
1101+ * - Version 1: Original behavior without parameter encoding
1102+ * - Version 2+ (default): Includes parameter encoding to prevent parameter smuggling
1103+ * @return {string } Hexadecimal signature
1104+ * @private
1105+ */
1106+ function api_sign_request ( params_to_sign , api_secret , signature_algorithm = null , signature_version = null ) {
1107+ if ( signature_version == null ) {
1108+ signature_version = config ( ) . signature_version || 2 ;
1109+ }
1110+ const to_sign = api_string_to_sign ( params_to_sign , signature_version ) ;
1111+ const algo = signature_algorithm || config ( ) . signature_algorithm || DEFAULT_SIGNATURE_ALGORITHM ;
1112+ return compute_hash ( to_sign + api_secret , algo , 'hex' ) ;
11221113}
11231114
11241115/**
@@ -1139,13 +1130,9 @@ function compute_hash(input, signature_algorithm, encoding) {
11391130
11401131function clear_blank ( hash ) {
11411132 let filtered_hash = { } ;
1142- entries ( hash ) . filter (
1143- ( [ k , v ] ) => utils . present ( v )
1144- ) . forEach (
1145- ( [ k , v ] ) => {
1146- filtered_hash [ k ] = v . filter ? v . filter ( x => x ) : v ;
1147- }
1148- ) ;
1133+ entries ( hash ) . filter ( ( [ k , v ] ) => utils . present ( v ) ) . forEach ( ( [ k , v ] ) => {
1134+ filtered_hash [ k ] = v . filter ? v . filter ( x => x ) : v ;
1135+ } ) ;
11491136 return filtered_hash ;
11501137}
11511138
@@ -1163,8 +1150,10 @@ function merge(hash1, hash2) {
11631150function sign_request ( params , options = { } ) {
11641151 let apiKey = ensureOption ( options , 'api_key' ) ;
11651152 let apiSecret = ensureOption ( options , 'api_secret' ) ;
1153+ let signature_algorithm = options . signature_algorithm ;
1154+ let signature_version = options . signature_version ;
11661155 params = exports . clear_blank ( params ) ;
1167- params . signature = exports . api_sign_request ( params , apiSecret ) ;
1156+ params . signature = exports . api_sign_request ( params , apiSecret , signature_algorithm , signature_version ) ;
11681157 params . api_key = apiKey ;
11691158 return params ;
11701159}
@@ -1556,9 +1545,7 @@ function generate_responsive_breakpoints_string(breakpoints) {
15561545 let breakpoint_settings = breakpoints [ j ] ;
15571546 if ( breakpoint_settings != null ) {
15581547 if ( breakpoint_settings . transformation ) {
1559- breakpoint_settings . transformation = utils . generate_transformation_string (
1560- clone ( breakpoint_settings . transformation )
1561- ) ;
1548+ breakpoint_settings . transformation = utils . generate_transformation_string ( clone ( breakpoint_settings . transformation ) ) ;
15621549 }
15631550 }
15641551 }
@@ -1568,11 +1555,9 @@ function generate_responsive_breakpoints_string(breakpoints) {
15681555function build_streaming_profiles_param ( options = { } ) {
15691556 let params = pickOnlyExistingValues ( options , "display_name" , "representations" ) ;
15701557 if ( isArray ( params . representations ) ) {
1571- params . representations = JSON . stringify ( params . representations . map (
1572- r => ( {
1573- transformation : utils . generate_transformation_string ( r . transformation )
1574- } )
1575- ) ) ;
1558+ params . representations = JSON . stringify ( params . representations . map ( r => ( {
1559+ transformation : utils . generate_transformation_string ( r . transformation )
1560+ } ) ) ) ;
15761561 }
15771562 return params ;
15781563}
@@ -1597,9 +1582,7 @@ function hashToParameters(hash) {
15971582 * @return {string } A URI query string.
15981583 */
15991584function hashToQuery ( hash ) {
1600- return hashToParameters ( hash ) . map (
1601- ( [ key , value ] ) => `${ querystring . escape ( key ) } =${ querystring . escape ( value ) } `
1602- ) . join ( '&' ) ;
1585+ return hashToParameters ( hash ) . map ( ( [ key , value ] ) => `${ querystring . escape ( key ) } =${ querystring . escape ( value ) } ` ) . join ( '&' ) ;
16031586}
16041587
16051588/**
@@ -1742,3 +1725,27 @@ Object.assign(module.exports, {
17421725 keys : source => Object . keys ( source ) ,
17431726 ensurePresenceOf
17441727} ) ;
1728+
1729+ /**
1730+ * Verifies an API response signature for a given public_id and version.
1731+ * Always uses signature version 1 for backward compatibility, matching the Ruby SDK.
1732+ * @param {string } public_id
1733+ * @param {string|number } version
1734+ * @param {string } signature
1735+ * @returns {boolean }
1736+ */
1737+ function verify_api_response_signature ( public_id , version , signature ) {
1738+ const api_secret = config ( ) . api_secret ;
1739+ const expected = exports . api_sign_request (
1740+ {
1741+ public_id,
1742+ version
1743+ } ,
1744+ api_secret ,
1745+ null ,
1746+ 1
1747+ ) ;
1748+ return signature === expected ;
1749+ }
1750+
1751+ exports . verify_api_response_signature = verify_api_response_signature ;
0 commit comments