2222 */
2323final class ApiUtilsTest extends UnitTestCase
2424{
25- public function tearDown ()
25+ public const API_SIGN_REQUEST_TEST_SECRET = 'hdcixPpR2iKERPwqvH6sHdK9cyac ' ;
26+ public const API_SIGN_REQUEST_CLOUD_NAME = 'dn6ot3ged ' ;
27+
28+ public function tearDown (): void
2629 {
2730 parent ::tearDown ();
2831
@@ -230,7 +233,7 @@ public function dataProviderSignParameters()
230233 ],
231234 [
232235 'value ' => ['p1 ' => 'v1=v2*|}{ & !@#$%^&*()_;/.,?>< \\/|_+a ' ],
233- 'result ' => 'bbdc631f4b490c0ba65722d8dbf9300d1fd98e86 ' ,
236+ 'result ' => 'ced1e363d8db0a8d7ebcfb9e67fadbf5ee78a0f1 ' ,
234237 ],
235238 ];
236239 }
@@ -276,12 +279,12 @@ public function dataProviderSignWithAlgorithmParameters()
276279 ],
277280 [
278281 'value ' => ['p1 ' => 'v1=v2*|}{ & !@#$%^&*()_;/.,?>< \\/|_+a ' ],
279- 'result ' => 'bbdc631f4b490c0ba65722d8dbf9300d1fd98e86 ' ,
282+ 'result ' => 'ced1e363d8db0a8d7ebcfb9e67fadbf5ee78a0f1 ' ,
280283 'signatureAlgorithm ' => 'sha1 ' ,
281284 ],
282285 [
283286 'value ' => ['p1 ' => 'v1=v2*|}{ & !@#$%^&*()_;/.,?>< \\/|_+a ' ],
284- 'result ' => '9cdbdd04f587b41db72d66437f6dac2a379cd899c0cf3c3430925b1beca6052d ' ,
287+ 'result ' => '0c06416c30bfc727eb2cbc9f93245be70bd6567c788b5bd93a3772e8253312bf ' ,
285288 'signatureAlgorithm ' => 'sha256 ' ,
286289 ],
287290 ];
@@ -310,13 +313,13 @@ public function testSignParametersWithExplicitSignatureAlgorithm($value, $result
310313 public function testApiSignRequestWithGlobalConfig ()
311314 {
312315 $ initialParams = [
313- 'cloud_name ' => ' dn6ot3ged ' ,
316+ 'cloud_name ' => self :: API_SIGN_REQUEST_CLOUD_NAME ,
314317 'timestamp ' => 1568810420 ,
315318316319 ];
317320
318321 $ params = $ initialParams ;
319- Configuration::instance ()->cloud ->apiSecret = ' hdcixPpR2iKERPwqvH6sHdK9cyac ' ;
322+ Configuration::instance ()->cloud ->apiSecret = self :: API_SIGN_REQUEST_TEST_SECRET ;
320323 Configuration::instance ()->cloud ->signatureAlgorithm = Utils::ALGO_SHA256 ;
321324 ApiUtils::signRequest ($ params , Configuration::instance ()->cloud );
322325 $ expected = '45ddaa4fa01f0c2826f32f669d2e4514faf275fe6df053f1a150e7beae58a3bd ' ;
@@ -335,15 +338,90 @@ public function testApiSignRequestWithGlobalConfig()
335338 public function testApiSignRequestWithExplicitConfig ()
336339 {
337340 $ params = [
338- 'cloud_name ' => ' dn6ot3ged ' ,
341+ 'cloud_name ' => self :: API_SIGN_REQUEST_CLOUD_NAME ,
339342 'timestamp ' => 1568810420 ,
340343341344 ];
342345
343- $ config = new Configuration ('cloudinary://key:hdcixPpR2iKERPwqvH6sHdK9cyac @test123 ' );
346+ $ config = new Configuration ('cloudinary://key: ' . self :: API_SIGN_REQUEST_TEST_SECRET . ' @test123 ' );
344347 $ config ->cloud ->signatureAlgorithm = Utils::ALGO_SHA256 ;
345348 ApiUtils::signRequest ($ params , $ config ->cloud );
346349 $ expected = '45ddaa4fa01f0c2826f32f669d2e4514faf275fe6df053f1a150e7beae58a3bd ' ;
347350 self ::assertEquals ($ expected , $ params ['signature ' ]);
348351 }
352+
353+ /**
354+ * Should prevent parameter smuggling via & characters in parameter values.
355+ */
356+ public function testApiSignRequestPreventsParameterSmuggling ()
357+ {
358+ // Test with notification_url containing & characters
359+ $ paramsWithAmpersand = [
360+ 'cloud_name ' => self ::API_SIGN_REQUEST_CLOUD_NAME ,
361+ 'timestamp ' => 1568810420 ,
362+ 'notification_url ' => 'https://fake.com/callback?a=1&tags=hello,world '
363+ ];
364+
365+ $ config = new Configuration ('cloudinary://key: ' . self ::API_SIGN_REQUEST_TEST_SECRET . '@test123 ' );
366+ ApiUtils::signRequest ($ paramsWithAmpersand , $ config ->cloud );
367+ $ signatureWithAmpersand = $ paramsWithAmpersand ['signature ' ];
368+
369+ // Test that attempting to smuggle parameters by splitting the notification_url fails
370+ $ paramsSmugggled = [
371+ 'cloud_name ' => self ::API_SIGN_REQUEST_CLOUD_NAME ,
372+ 'timestamp ' => 1568810420 ,
373+ 'notification_url ' => 'https://fake.com/callback?a=1 ' ,
374+ 'tags ' => 'hello,world ' // This would be smuggled if & encoding didn't work
375+ ];
376+
377+ ApiUtils::signRequest ($ paramsSmugggled , $ config ->cloud );
378+ $ signatureSmugggled = $ paramsSmugggled ['signature ' ];
379+
380+ // The signatures should be different, proving that parameter smuggling is prevented
381+ self ::assertNotEquals ($ signatureWithAmpersand , $ signatureSmugggled ,
382+ 'Signatures should be different to prevent parameter smuggling ' );
383+
384+ // Verify the expected signature for the properly encoded case
385+ $ expectedSignature = '4fdf465dd89451cc1ed8ec5b3e314e8a51695704 ' ;
386+ self ::assertEquals ($ expectedSignature , $ signatureWithAmpersand );
387+
388+ // Verify the expected signature for the smuggled parameters case
389+ $ expectedSmuggledSignature = '7b4e3a539ff1fa6e6700c41b3a2ee77586a025f9 ' ;
390+ self ::assertEquals ($ expectedSmuggledSignature , $ signatureSmugggled );
391+ }
392+
393+ /**
394+ * Should apply the configured signature version from CloudConfig.
395+ */
396+ public function testConfiguredSignatureVersionIsApplied ()
397+ {
398+ $ params = [
399+ 'cloud_name ' => self ::API_SIGN_REQUEST_CLOUD_NAME ,
400+ 'timestamp ' => 1568810420 ,
401+ 'notification_url ' => 'https://fake.com/callback?a=1&tags=hello,world '
402+ ];
403+
404+ $ config = new Configuration ('cloudinary://key: ' . self ::API_SIGN_REQUEST_TEST_SECRET . '@test123 ' );
405+
406+ // Test with signature version 1 (legacy behavior - no URL encoding)
407+ $ config ->cloud ->signatureVersion = 1 ;
408+ $ paramsV1 = $ params ;
409+ ApiUtils::signRequest ($ paramsV1 , $ config ->cloud );
410+ $ signatureV1 = $ paramsV1 ['signature ' ];
411+
412+ // Test with signature version 2 (current behavior - with URL encoding)
413+ $ config ->cloud ->signatureVersion = 2 ;
414+ $ paramsV2 = $ params ;
415+ ApiUtils::signRequest ($ paramsV2 , $ config ->cloud );
416+ $ signatureV2 = $ paramsV2 ['signature ' ];
417+
418+ // Signatures should be different, proving the version setting is applied
419+ self ::assertNotEquals ($ signatureV1 , $ signatureV2 ,
420+ 'Signature versions should produce different results ' );
421+
422+ // Version 2 should match the expected encoded signature
423+ $ expectedV2Signature = '4fdf465dd89451cc1ed8ec5b3e314e8a51695704 ' ;
424+ self ::assertEquals ($ expectedV2Signature , $ signatureV2 ,
425+ 'Version 2 should match expected encoded signature ' );
426+ }
349427}
0 commit comments