@@ -544,9 +544,7 @@ function generate_transformation_string(options) {
544
544
let base_transformations = toArray ( consumeOption ( options , "transformation" , [ ] ) ) ;
545
545
let named_transformation = [ ] ;
546
546
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 } ) ) ;
550
548
} else {
551
549
named_transformation = base_transformations . join ( "." ) ;
552
550
base_transformations = [ ] ;
@@ -555,9 +553,7 @@ function generate_transformation_string(options) {
555
553
if ( isArray ( effect ) ) {
556
554
effect = effect . join ( ":" ) ;
557
555
} else if ( isObject ( effect ) ) {
558
- effect = entries ( effect ) . map (
559
- ( [ key , value ] ) => `${ key } :${ value } `
560
- ) ;
556
+ effect = entries ( effect ) . map ( ( [ key , value ] ) => `${ key } :${ value } ` ) ;
561
557
}
562
558
let border = consumeOption ( options , "border" ) ;
563
559
if ( isObject ( border ) ) {
@@ -634,9 +630,7 @@ function generate_transformation_string(options) {
634
630
. map ( ( [ key , value ] ) => {
635
631
delete options [ key ] ;
636
632
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 ( ',' ) ;
640
634
641
635
let transformations = entries ( params )
642
636
. filter ( ( [ key , value ] ) => utils . present ( value ) )
@@ -649,8 +643,7 @@ function generate_transformation_string(options) {
649
643
base_transformations . push ( transformations ) ;
650
644
transformations = base_transformations ;
651
645
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 ;
654
647
655
648
transformations . push ( utils . generate_transformation_string ( clone ( responsive_width_transformation ) ) ) ;
656
649
}
@@ -745,27 +738,7 @@ function updateable_resource_params(options, params = {}) {
745
738
* A list of keys used by the url() function.
746
739
* @private
747
740
*/
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' ] ;
769
742
770
743
/**
771
744
* Create a new object with only URL parameters
@@ -930,9 +903,7 @@ function url(public_id, options = {}) {
930
903
urlAnalytics
931
904
} ;
932
905
933
- let analyticsOptions = getAnalyticsOptions (
934
- Object . assign ( { } , options , sdkVersions )
935
- ) ;
906
+ let analyticsOptions = getAnalyticsOptions ( Object . assign ( { } , options , sdkVersions ) ) ;
936
907
937
908
let sdkAnalyticsSignature = getSDKAnalyticsSignature ( analyticsOptions ) ;
938
909
@@ -1033,16 +1004,7 @@ function finalize_resource_type(resource_type, type, url_suffix, use_root_path,
1033
1004
// if cdn_domain is true uses a[1-5].cname for http.
1034
1005
// For https, uses the same naming scheme as 1 for shared distribution and as 2 for private distribution.
1035
1006
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 ) {
1046
1008
let prefix ;
1047
1009
if ( cloud_name . indexOf ( "/" ) === 0 ) {
1048
1010
return '/res' + cloud_name ;
@@ -1112,13 +1074,42 @@ function signed_preloaded_image(result) {
1112
1074
return `${ result . resource_type } /upload/v${ result . version } /${ filter ( [ result . public_id , result . format ] , utils . present ) . join ( "." ) } #${ result . signature } ` ;
1113
1075
}
1114
1076
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' ) ;
1122
1113
}
1123
1114
1124
1115
/**
@@ -1139,13 +1130,9 @@ function compute_hash(input, signature_algorithm, encoding) {
1139
1130
1140
1131
function clear_blank ( hash ) {
1141
1132
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
+ } ) ;
1149
1136
return filtered_hash ;
1150
1137
}
1151
1138
@@ -1163,8 +1150,10 @@ function merge(hash1, hash2) {
1163
1150
function sign_request ( params , options = { } ) {
1164
1151
let apiKey = ensureOption ( options , 'api_key' ) ;
1165
1152
let apiSecret = ensureOption ( options , 'api_secret' ) ;
1153
+ let signature_algorithm = options . signature_algorithm ;
1154
+ let signature_version = options . signature_version ;
1166
1155
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 ) ;
1168
1157
params . api_key = apiKey ;
1169
1158
return params ;
1170
1159
}
@@ -1556,9 +1545,7 @@ function generate_responsive_breakpoints_string(breakpoints) {
1556
1545
let breakpoint_settings = breakpoints [ j ] ;
1557
1546
if ( breakpoint_settings != null ) {
1558
1547
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 ) ) ;
1562
1549
}
1563
1550
}
1564
1551
}
@@ -1568,11 +1555,9 @@ function generate_responsive_breakpoints_string(breakpoints) {
1568
1555
function build_streaming_profiles_param ( options = { } ) {
1569
1556
let params = pickOnlyExistingValues ( options , "display_name" , "representations" ) ;
1570
1557
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
+ } ) ) ) ;
1576
1561
}
1577
1562
return params ;
1578
1563
}
@@ -1597,9 +1582,7 @@ function hashToParameters(hash) {
1597
1582
* @return {string } A URI query string.
1598
1583
*/
1599
1584
function 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 ( '&' ) ;
1603
1586
}
1604
1587
1605
1588
/**
@@ -1742,3 +1725,27 @@ Object.assign(module.exports, {
1742
1725
keys : source => Object . keys ( source ) ,
1743
1726
ensurePresenceOf
1744
1727
} ) ;
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