From fa194f3c815494d9b3d63bbfb3f35c9b033b3f7b Mon Sep 17 00:00:00 2001 From: adimiz1 Date: Mon, 16 Jun 2025 01:16:06 +0300 Subject: [PATCH 1/6] Fix signature function with escaping characters --- api/lib/uploader/utils.dart | 32 +++++++++++++++++++------------- api/test/uploader_test.dart | 12 ++++++++++++ 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/api/lib/uploader/utils.dart b/api/lib/uploader/utils.dart index fac3cce..f9443c3 100644 --- a/api/lib/uploader/utils.dart +++ b/api/lib/uploader/utils.dart @@ -10,35 +10,41 @@ class Utils { 'filename', ]; - static String apiSignRequest( - Map paramsMap, String apiSecret) { + static String apiSignRequest(Map paramsMap, String apiSecret) { List paramsArr = []; - paramsMap.removeWhere((key, value) => value == null); - paramsMap.removeWhere( - (key, value) => value == null || _excludeKeys.contains(key)); - var sortedParams = paramsMap.keys.whereType().toList()..sort(); - for (var key in sortedParams) { + + // Remove null values and excluded keys + paramsMap.removeWhere((key, value) => value == null || _excludeKeys.contains(key)); + + // Sort keys + var sortedKeys = paramsMap.keys.whereType().toList()..sort(); + + for (var key in sortedKeys) { var value = paramsMap[key]; String? paramValue; + if (value is List) { if (value.isNotEmpty) { - paramValue = value.toString(); //.join(','); + paramValue = value.join(','); // You can also use value.toString() if that's your format } else { continue; } - } else { - if (value != null) { - paramValue = value.toString(); - } + } else if (value != null) { + paramValue = value.toString(); } + if (paramValue != null) { - paramsArr.add('$key=${paramValue.replaceAll(r'\', '')}'); + // Replace all & characters in the value with %26 + paramValue = paramValue.replaceAll('&', '%26'); + paramsArr.add('$key=$paramValue'); } } + var toSign = '${paramsArr.join('&')}$apiSecret'; return hex.encode(sha1.convert(utf8.encode(toSign)).bytes); } + static bool isRemoteUrl(String value) { return RegExp( r'ftp:.*|https?:.*|s3:.*|gs:.*|data:([\w-]+/[\w-]+)?(;[\w-]+=[\w-]+)*;base64,([a-zA-Z0-9/+ =]+)') diff --git a/api/test/uploader_test.dart b/api/test/uploader_test.dart index fca7a85..e21efcd 100644 --- a/api/test/uploader_test.dart +++ b/api/test/uploader_test.dart @@ -549,6 +549,18 @@ void main() { var result = resultOrThrow(response?.data); assert(result.playbackUrl != null); }); + + test('Test signature with escaping characters', () { + final signatureWithEscapingCharacters = 'c9e94ac9a6787698de868e387e26dc0f3422b2b2'; + final toSign = { + 'public_id': 'publicid&tags=blabla', + }; + + final apiSecret = 'your_api_secret'; // Replace with actual secret or mock + final signature = Utils.apiSignRequest(toSign, apiSecret); + + expect(signature, isNot(equals(signatureWithEscapingCharacters))); + }); } validateSignature(UploadResult result) { From 47605c2308181a0a58bfc7deaff72a6649a1d569 Mon Sep 17 00:00:00 2001 From: adimiz1 Date: Mon, 16 Jun 2025 01:29:09 +0300 Subject: [PATCH 2/6] Fix signature function --- api/lib/uploader/utils.dart | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/api/lib/uploader/utils.dart b/api/lib/uploader/utils.dart index f9443c3..1c20d65 100644 --- a/api/lib/uploader/utils.dart +++ b/api/lib/uploader/utils.dart @@ -10,32 +10,35 @@ class Utils { 'filename', ]; - static String apiSignRequest(Map paramsMap, String apiSecret) { + static String apiSignRequest( + Map paramsMap, String apiSecret) { List paramsArr = []; - // Remove null values and excluded keys - paramsMap.removeWhere((key, value) => value == null || _excludeKeys.contains(key)); + paramsMap.removeWhere((key, value) => value == null); + paramsMap.removeWhere( + (key, value) => value == null || _excludeKeys.contains(key)); - // Sort keys - var sortedKeys = paramsMap.keys.whereType().toList()..sort(); + var sortedParams = paramsMap.keys.whereType().toList()..sort(); - for (var key in sortedKeys) { + for (var key in sortedParams) { var value = paramsMap[key]; String? paramValue; if (value is List) { if (value.isNotEmpty) { - paramValue = value.join(','); // You can also use value.toString() if that's your format + paramValue = value.toString(); // Keep same structure } else { continue; } - } else if (value != null) { - paramValue = value.toString(); + } else { + if (value != null) { + paramValue = value.toString(); + } } if (paramValue != null) { - // Replace all & characters in the value with %26 - paramValue = paramValue.replaceAll('&', '%26'); + // Strip backslashes, then replace & with %26 + paramValue = paramValue.replaceAll(r'\', '').replaceAll('&', '%26'); paramsArr.add('$key=$paramValue'); } } @@ -44,7 +47,6 @@ class Utils { return hex.encode(sha1.convert(utf8.encode(toSign)).bytes); } - static bool isRemoteUrl(String value) { return RegExp( r'ftp:.*|https?:.*|s3:.*|gs:.*|data:([\w-]+/[\w-]+)?(;[\w-]+=[\w-]+)*;base64,([a-zA-Z0-9/+ =]+)') From fddfb679d8e1b0857b80d7c140f6770f8adfc5e6 Mon Sep 17 00:00:00 2001 From: adimiz1 Date: Mon, 16 Jun 2025 11:09:09 +0300 Subject: [PATCH 3/6] Improve tests --- api/lib/uploader/utils.dart | 39 +++++++------------------------------ api/test/uploader_test.dart | 33 +++++++++++++++++++++++++------ 2 files changed, 34 insertions(+), 38 deletions(-) diff --git a/api/lib/uploader/utils.dart b/api/lib/uploader/utils.dart index 1c20d65..50a428b 100644 --- a/api/lib/uploader/utils.dart +++ b/api/lib/uploader/utils.dart @@ -10,41 +10,16 @@ class Utils { 'filename', ]; - static String apiSignRequest( - Map paramsMap, String apiSecret) { - List paramsArr = []; - + static String apiSignRequest(Map paramsMap, String apiSecret) { paramsMap.removeWhere((key, value) => value == null); - paramsMap.removeWhere( - (key, value) => value == null || _excludeKeys.contains(key)); - - var sortedParams = paramsMap.keys.whereType().toList()..sort(); - - for (var key in sortedParams) { - var value = paramsMap[key]; - String? paramValue; + paramsMap.removeWhere((key, value) => value == null || _excludeKeys.contains(key)); - if (value is List) { - if (value.isNotEmpty) { - paramValue = value.toString(); // Keep same structure - } else { - continue; - } - } else { - if (value != null) { - paramValue = value.toString(); - } - } - - if (paramValue != null) { - // Strip backslashes, then replace & with %26 - paramValue = paramValue.replaceAll(r'\', '').replaceAll('&', '%26'); - paramsArr.add('$key=$paramValue'); - } - } + String queryString = (paramsMap.keys.whereType().toList()..sort()) + .where((key) => paramsMap[key] is List ? (paramsMap[key] as List).isNotEmpty : paramsMap[key] != null) + .map((key) => '$key=${paramsMap[key].toString().replaceAll(r'\', '').replaceAll('&', '%26')}') + .join('&'); - var toSign = '${paramsArr.join('&')}$apiSecret'; - return hex.encode(sha1.convert(utf8.encode(toSign)).bytes); + return hex.encode(sha1.convert(utf8.encode(queryString + apiSecret)).bytes); } static bool isRemoteUrl(String value) { diff --git a/api/test/uploader_test.dart b/api/test/uploader_test.dart index e21efcd..e61f61b 100644 --- a/api/test/uploader_test.dart +++ b/api/test/uploader_test.dart @@ -551,15 +551,36 @@ void main() { }); test('Test signature with escaping characters', () { - final signatureWithEscapingCharacters = 'c9e94ac9a6787698de868e387e26dc0f3422b2b2'; - final toSign = { - 'public_id': 'publicid&tags=blabla', + const cloudName = 'dn6ot3ged'; + const secret = 'hdcixPpR2iKERPwqvH6sHdK9cyac'; + + final paramsWithAmpersand = { + 'cloud_name': cloudName, + 'timestamp': 1568810420, + 'notification_url': 'https://fake.com/callback?a=1&tags=hello,world' + }; + + final signatureWithAmpersand = Utils.apiSignRequest(paramsWithAmpersand, secret); + + final paramsSmuggled = { + 'cloud_name': cloudName, + 'timestamp': 1568810420, + 'notification_url': 'https://fake.com/callback?a=1', + 'tags': 'hello,world' }; - final apiSecret = 'your_api_secret'; // Replace with actual secret or mock - final signature = Utils.apiSignRequest(toSign, apiSecret); + final signatureSmuggled = Utils.apiSignRequest(paramsSmuggled, secret); + + // Ensure different signatures to prevent smuggling + expect(signatureWithAmpersand, isNot(equals(signatureSmuggled)), + reason: 'Signatures should be different to prevent parameter smuggling'); + + // Expected known outputs (from Java reference) + const expectedSignature = '4fdf465dd89451cc1ed8ec5b3e314e8a51695704'; + expect(signatureWithAmpersand, equals(expectedSignature)); - expect(signature, isNot(equals(signatureWithEscapingCharacters))); + const expectedSmuggledSignature = '7b4e3a539ff1fa6e6700c41b3a2ee77586a025f9'; + expect(signatureSmuggled, equals(expectedSmuggledSignature)); }); } From 05810f06bffefd4b9aebc56394b952f3818a0ca4 Mon Sep 17 00:00:00 2001 From: adimiz1 Date: Tue, 17 Jun 2025 16:21:33 +0300 Subject: [PATCH 4/6] Add signatureVersion --- api/lib/uploader/uploader_utils.dart | 2 +- api/lib/uploader/utils.dart | 13 ++++++++----- api/test/uploader_test.dart | 11 ++++++----- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/api/lib/uploader/uploader_utils.dart b/api/lib/uploader/uploader_utils.dart index c8eb268..46b4874 100644 --- a/api/lib/uploader/uploader_utils.dart +++ b/api/lib/uploader/uploader_utils.dart @@ -57,7 +57,7 @@ class UploaderUtils { paramsMap['timestamp'] = (DateTime.now().millisecondsSinceEpoch / 1000).toString(); paramsMap['signature'] = - Utils.apiSignRequest(paramsMap, config.apiSecret!); + Utils.apiSignRequest(paramsMap, config.apiSecret!, cloudinary.config.cloudConfig.signatureVersion); paramsMap['api_key'] = config.apiKey; if (paramsMap['unsigned'] != null) { paramsMap.remove('unsigned'); diff --git a/api/lib/uploader/utils.dart b/api/lib/uploader/utils.dart index 50a428b..763b329 100644 --- a/api/lib/uploader/utils.dart +++ b/api/lib/uploader/utils.dart @@ -10,15 +10,18 @@ class Utils { 'filename', ]; - static String apiSignRequest(Map paramsMap, String apiSecret) { - paramsMap.removeWhere((key, value) => value == null); + static String apiSignRequest(Map paramsMap, String apiSecret, int signatureVersion) { paramsMap.removeWhere((key, value) => value == null || _excludeKeys.contains(key)); String queryString = (paramsMap.keys.whereType().toList()..sort()) .where((key) => paramsMap[key] is List ? (paramsMap[key] as List).isNotEmpty : paramsMap[key] != null) - .map((key) => '$key=${paramsMap[key].toString().replaceAll(r'\', '').replaceAll('&', '%26')}') - .join('&'); - + .map((key) { + var value = paramsMap[key].toString().replaceAll(r'\', ''); + if (signatureVersion == 2) { + value = value.replaceAll('&', '%26'); + } + return '$key=$value'; + }).join('&'); return hex.encode(sha1.convert(utf8.encode(queryString + apiSecret)).bytes); } diff --git a/api/test/uploader_test.dart b/api/test/uploader_test.dart index e61f61b..036c1fc 100644 --- a/api/test/uploader_test.dart +++ b/api/test/uploader_test.dart @@ -560,7 +560,7 @@ void main() { 'notification_url': 'https://fake.com/callback?a=1&tags=hello,world' }; - final signatureWithAmpersand = Utils.apiSignRequest(paramsWithAmpersand, secret); + final signatureWithAmpersand = Utils.apiSignRequest(paramsWithAmpersand, secret, cloudinary.config.cloudConfig.signatureVersion); final paramsSmuggled = { 'cloud_name': cloudName, @@ -569,18 +569,19 @@ void main() { 'tags': 'hello,world' }; - final signatureSmuggled = Utils.apiSignRequest(paramsSmuggled, secret); + final signatureSmuggled = Utils.apiSignRequest(paramsSmuggled, secret, cloudinary.config.cloudConfig.signatureVersion); - // Ensure different signatures to prevent smuggling expect(signatureWithAmpersand, isNot(equals(signatureSmuggled)), reason: 'Signatures should be different to prevent parameter smuggling'); - // Expected known outputs (from Java reference) const expectedSignature = '4fdf465dd89451cc1ed8ec5b3e314e8a51695704'; expect(signatureWithAmpersand, equals(expectedSignature)); const expectedSmuggledSignature = '7b4e3a539ff1fa6e6700c41b3a2ee77586a025f9'; expect(signatureSmuggled, equals(expectedSmuggledSignature)); + + final versionOneSignature = Utils.apiSignRequest(paramsSmuggled, secret, 1); + expect(versionOneSignature, equals(signatureSmuggled)); }); } @@ -590,7 +591,7 @@ validateSignature(UploadResult result) { toSign['version'] = result.version.toString(); var expectedSignature = - Utils.apiSignRequest(toSign, cloudinary.config.cloudConfig.apiSecret!); + Utils.apiSignRequest(toSign, cloudinary.config.cloudConfig.apiSecret!, cloudinary.config.cloudConfig.signatureVersion); assert(result.signature == expectedSignature); } From e72ef8822a917b055857a3c54923237a09d20991 Mon Sep 17 00:00:00 2001 From: adimiz1 Date: Tue, 17 Jun 2025 16:41:48 +0300 Subject: [PATCH 5/6] Update url_gen version --- api/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/pubspec.yaml b/api/pubspec.yaml index 2b042aa..69aa6b4 100644 --- a/api/pubspec.yaml +++ b/api/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: crypto: ^3.0.3 convert: ^3.1.1 http_parser: ^4.0.2 - cloudinary_url_gen: ^1.7.0 + cloudinary_url_gen: ^1.8.0 dev_dependencies: lints: ^3.0.0 From 674ccea05aa4a65ee197fe4eaafe9063b2dd2033 Mon Sep 17 00:00:00 2001 From: adimiz1 Date: Tue, 17 Jun 2025 16:57:54 +0300 Subject: [PATCH 6/6] Add signature version as optional --- api/lib/uploader/uploader_utils.dart | 2 +- api/lib/uploader/utils.dart | 3 ++- api/test/uploader_test.dart | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/api/lib/uploader/uploader_utils.dart b/api/lib/uploader/uploader_utils.dart index 46b4874..2f2dac9 100644 --- a/api/lib/uploader/uploader_utils.dart +++ b/api/lib/uploader/uploader_utils.dart @@ -57,7 +57,7 @@ class UploaderUtils { paramsMap['timestamp'] = (DateTime.now().millisecondsSinceEpoch / 1000).toString(); paramsMap['signature'] = - Utils.apiSignRequest(paramsMap, config.apiSecret!, cloudinary.config.cloudConfig.signatureVersion); + Utils.apiSignRequest(paramsMap, config.apiSecret!, signatureVersion: cloudinary.config.cloudConfig.signatureVersion); paramsMap['api_key'] = config.apiKey; if (paramsMap['unsigned'] != null) { paramsMap.remove('unsigned'); diff --git a/api/lib/uploader/utils.dart b/api/lib/uploader/utils.dart index 763b329..e04b45b 100644 --- a/api/lib/uploader/utils.dart +++ b/api/lib/uploader/utils.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:math'; +import 'package:cloudinary_url_gen/config/cloud_config.dart'; import 'package:convert/convert.dart'; import 'package:crypto/crypto.dart'; @@ -10,7 +11,7 @@ class Utils { 'filename', ]; - static String apiSignRequest(Map paramsMap, String apiSecret, int signatureVersion) { + static String apiSignRequest(Map paramsMap, String apiSecret, {int? signatureVersion = defaultSignatureVersion}) { paramsMap.removeWhere((key, value) => value == null || _excludeKeys.contains(key)); String queryString = (paramsMap.keys.whereType().toList()..sort()) diff --git a/api/test/uploader_test.dart b/api/test/uploader_test.dart index 036c1fc..552fc67 100644 --- a/api/test/uploader_test.dart +++ b/api/test/uploader_test.dart @@ -560,7 +560,7 @@ void main() { 'notification_url': 'https://fake.com/callback?a=1&tags=hello,world' }; - final signatureWithAmpersand = Utils.apiSignRequest(paramsWithAmpersand, secret, cloudinary.config.cloudConfig.signatureVersion); + final signatureWithAmpersand = Utils.apiSignRequest(paramsWithAmpersand, secret); final paramsSmuggled = { 'cloud_name': cloudName, @@ -569,7 +569,7 @@ void main() { 'tags': 'hello,world' }; - final signatureSmuggled = Utils.apiSignRequest(paramsSmuggled, secret, cloudinary.config.cloudConfig.signatureVersion); + final signatureSmuggled = Utils.apiSignRequest(paramsSmuggled, secret); expect(signatureWithAmpersand, isNot(equals(signatureSmuggled)), reason: 'Signatures should be different to prevent parameter smuggling'); @@ -580,7 +580,7 @@ void main() { const expectedSmuggledSignature = '7b4e3a539ff1fa6e6700c41b3a2ee77586a025f9'; expect(signatureSmuggled, equals(expectedSmuggledSignature)); - final versionOneSignature = Utils.apiSignRequest(paramsSmuggled, secret, 1); + final versionOneSignature = Utils.apiSignRequest(paramsSmuggled, secret, signatureVersion: 1); expect(versionOneSignature, equals(signatureSmuggled)); }); } @@ -591,7 +591,7 @@ validateSignature(UploadResult result) { toSign['version'] = result.version.toString(); var expectedSignature = - Utils.apiSignRequest(toSign, cloudinary.config.cloudConfig.apiSecret!, cloudinary.config.cloudConfig.signatureVersion); + Utils.apiSignRequest(toSign, cloudinary.config.cloudConfig.apiSecret!); assert(result.signature == expectedSignature); }