@@ -627,4 +627,169 @@ class SecretPayload
627627 end
628628 end
629629
630+ describe 'path parameter validation' do
631+ # Runs the command against a stub and returns the path that went out on the wire.
632+ # WebMock treats '+' and '%2B' as equal, so the assertions check the expanded URL instead.
633+ def wire_path ( command )
634+ stub_request ( :get , /www\. googleapis\. com/ ) . to_return ( status : [ 200 , '' ] )
635+ command . options . retries = 0
636+ command . execute ( client )
637+ command . url . path
638+ end
639+
640+ context 'with simple path variables' do
641+ # Source template specification:
642+ # Pattern: v1/projects/{project}/locations/{location}/webhooks/{webhook}
643+ # Variable constraints (RFC 6570 simple expansion):
644+ # - Must not be exactly a traversal segment ('.', '..')
645+ # - Reserved characters such as '/', '?' and '#' are percent-encoded by Addressable
646+ let ( :template ) do
647+ Addressable ::Template . new (
648+ 'https://www.googleapis.com/v1/projects/{project}/locations/{location}/webhooks/{webhook}'
649+ )
650+ end
651+
652+ def webhook_command ( webhook )
653+ command = Google ::Apis ::Core ::HttpCommand . new ( :get , template )
654+ command . params [ :project ] = 'sys-prod-123'
655+ command . params [ :location ] = 'us-central1'
656+ command . params [ :webhook ] = webhook
657+ command
658+ end
659+
660+ it 'should allow safe parameter values' do
661+ expect ( wire_path ( webhook_command ( 'billing-webhook' ) ) )
662+ . to eq ( '/v1/projects/sys-prod-123/locations/us-central1/webhooks/billing-webhook' )
663+ end
664+
665+ it 'should percent-encode slashes in simple parameters on the wire' do
666+ expect ( wire_path ( webhook_command ( 'parent/child-webhook' ) ) )
667+ . to eq ( '/v1/projects/sys-prod-123/locations/us-central1/webhooks/parent%2Fchild-webhook' )
668+ end
669+
670+ it 'should percent-encode slashes in Cloud Storage object names on the wire' do
671+ storage_template = Addressable ::Template . new ( 'https://www.googleapis.com/storage/v1/b/{bucket}/o/{object}' )
672+ command = Google ::Apis ::Core ::HttpCommand . new ( :get , storage_template )
673+ command . params [ :bucket ] = 'bk'
674+ command . params [ :object ] = 'folder/file.txt'
675+ expect ( wire_path ( command ) ) . to eq ( '/storage/v1/b/bk/o/folder%2Ffile.txt' )
676+ end
677+
678+ it 'should reject dot or double-dot in simple parameters' do
679+ command = webhook_command ( '..' )
680+ command . options . retries = 0
681+ expect { command . execute ( client ) } . to raise_error ( Google ::Apis ::ClientError , "Invalid value for webhook '..'." )
682+ end
683+
684+ it 'should percent-encode percent signs in simple parameters on the wire' do
685+ expect ( wire_path ( webhook_command ( '%2e%2e' ) ) )
686+ . to eq ( '/v1/projects/sys-prod-123/locations/us-central1/webhooks/%252e%252e' )
687+ expect ( wire_path ( webhook_command ( '%2e' ) ) )
688+ . to eq ( '/v1/projects/sys-prod-123/locations/us-central1/webhooks/%252e' )
689+ end
690+
691+ it 'should percent-encode query and fragment characters in simple parameters on the wire' do
692+ expect ( wire_path ( webhook_command ( 'billing-webhook?key=val' ) ) )
693+ . to eq ( '/v1/projects/sys-prod-123/locations/us-central1/webhooks/billing-webhook%3Fkey%3Dval' )
694+ expect ( wire_path ( webhook_command ( 'billing-webhook#fragment' ) ) )
695+ . to eq ( '/v1/projects/sys-prod-123/locations/us-central1/webhooks/billing-webhook%23fragment' )
696+ end
697+ end
698+
699+ context 'with reserved path variables' do
700+ # Source template specification:
701+ # Pattern: v1/{+parent}/indexes
702+ # Variable constraints (RFC 6570 reserved expansion):
703+ # - Allows slashes (multiple path segments), including empty segments
704+ # - Must not contain traversal segments ('.', '..')
705+ # - Every character except '/' and unreserved characters is percent-encoded
706+ let ( :template ) { Addressable ::Template . new ( 'https://www.googleapis.com/v1/{+parent}/indexes' ) }
707+
708+ def parent_command ( parent )
709+ command = Google ::Apis ::Core ::HttpCommand . new ( :get , template )
710+ command . params [ :parent ] = parent
711+ command
712+ end
713+
714+ def expect_rejected ( parent , message )
715+ command = parent_command ( parent )
716+ command . options . retries = 0
717+ expect { command . execute ( client ) } . to raise_error ( Google ::Apis ::ClientError , message )
718+ end
719+
720+ it 'should allow safe double wildcard paths' do
721+ expect ( wire_path ( parent_command ( 'projects/sys-prod-123/databases/default/documents/doc-1' ) ) )
722+ . to eq ( '/v1/projects/sys-prod-123/databases/default/documents/doc-1/indexes' )
723+ end
724+
725+ it 'should percent-encode query characters in reserved parameters' do
726+ expect ( wire_path ( parent_command ( 'projects/sys-prod-123?key=val' ) ) )
727+ . to eq ( '/v1/projects/sys-prod-123%3Fkey%3Dval/indexes' )
728+ end
729+
730+ it 'should percent-encode fragment characters in reserved parameters' do
731+ expect ( wire_path ( parent_command ( 'projects/sys-prod-123#fragment' ) ) )
732+ . to eq ( '/v1/projects/sys-prod-123%23fragment/indexes' )
733+ end
734+
735+ it 'should percent-encode colons in reserved parameters' do
736+ expect ( wire_path ( parent_command ( 'projects/p:undelete' ) ) ) . to eq ( '/v1/projects/p%3Aundelete/indexes' )
737+ end
738+
739+ it 'should percent-encode other reserved characters in reserved parameters' do
740+ expect ( wire_path ( parent_command ( 'projects/p/topics/a+b@c,d=e' ) ) )
741+ . to eq ( '/v1/projects/p/topics/a%2Bb%40c%2Cd%3De/indexes' )
742+ end
743+
744+ it 'should percent-encode braces in reserved parameters' do
745+ expect ( wire_path ( parent_command ( 'projects/{x}' ) ) ) . to eq ( '/v1/projects/%7Bx%7D/indexes' )
746+ end
747+
748+ it 'should not normalize unicode in reserved parameters by default' do
749+ expect ( wire_path ( parent_command ( "projects/Cafe\u0301 " ) ) ) . to eq ( '/v1/projects/Cafe%CC%81/indexes' )
750+ end
751+
752+ it 'should normalize unicode in reserved parameters when requested' do
753+ command = parent_command ( "projects/Cafe\u0301 " )
754+ command . options . normalize_unicode = true
755+ expect ( wire_path ( command ) ) . to eq ( '/v1/projects/Caf%C3%A9/indexes' )
756+ end
757+
758+ it 'should reject relative traversals inside the wildcard' do
759+ expect_rejected ( 'projects/sys-prod-123/databases/default/documents/doc-1/../../default' ,
760+ "Value for parent must not contain segments that are exactly '..'." )
761+ end
762+
763+ it 'should reject traversals escaping the left parameter boundary' do
764+ expect_rejected ( 'projects/sys-prod-123/databases/default/documents/doc-1/../../../../../../../escape-db' ,
765+ "Value for parent must not contain segments that are exactly '..'." )
766+ end
767+
768+ it 'should reject parameter values starting with parent directory traversals' do
769+ expect_rejected ( '../escape-db' , "Value for parent must not contain segments that are exactly '..'." )
770+ end
771+
772+ it 'should reject invalid segments like single dot' do
773+ expect_rejected ( 'projects/sys-prod-123/./databases/default' ,
774+ "Value for parent must not contain segments that are exactly '.'." )
775+ end
776+
777+ it 'should percent-encode percent signs in reserved parameters on the wire' do
778+ expect ( wire_path ( parent_command ( 'projects/p/databases/d/documents/doc/%2e%2e/doc2' ) ) )
779+ . to eq ( '/v1/projects/p/databases/d/documents/doc/%252e%252e/doc2/indexes' )
780+ expect ( wire_path ( parent_command ( 'projects/p/databases/d/documents/doc/%2e/a' ) ) )
781+ . to eq ( '/v1/projects/p/databases/d/documents/doc/%252e/a/indexes' )
782+ expect ( wire_path ( parent_command ( 'projects/p/databases/d/documents/doc/..%2f..%2fescape-db' ) ) )
783+ . to eq ( '/v1/projects/p/databases/d/documents/doc/..%252f..%252fescape-db/indexes' )
784+ expect ( wire_path ( parent_command ( 'projects/p/databases/d/documents/doc/%2e%2e%2f%2e%2e%2fescape-db' ) ) )
785+ . to eq ( '/v1/projects/p/databases/d/documents/doc/%252e%252e%252f%252e%252e%252fescape-db/indexes' )
786+ end
787+
788+ it 'should allow empty segments in reserved parameters' do
789+ expect ( wire_path ( parent_command ( 'projects/sys-prod-123//databases/default' ) ) )
790+ . to eq ( '/v1/projects/sys-prod-123//databases/default/indexes' )
791+ expect ( wire_path ( parent_command ( 'projects/sys-prod-123/' ) ) ) . to eq ( '/v1/projects/sys-prod-123//indexes' )
792+ end
793+ end
794+ end
630795end
0 commit comments