@@ -20,6 +20,10 @@ namespace Azure.Containers.ContainerRegistry
2020 public class ContainerRegistryContentClient
2121 {
2222 private const int DefaultChunkSize = 4 * 1024 * 1024 ; // 4MB
23+ private const int MaxManifestSize = 4 * 1024 * 1024 ;
24+
25+ private const string InvalidContentLengthMessage = "Missing or invalid 'Content-Length' header in the response." ;
26+ private const string InvalidContentRangeMessage = "Missing or invalid 'Content-Range' header in the response." ;
2327
2428 private readonly Uri _endpoint ;
2529 private readonly string _registryName ;
@@ -532,10 +536,22 @@ private static string GetContentRange(long offset, long length)
532536 return FormattableString . Invariant ( $ "{ offset } -{ endRange } ") ;
533537 }
534538
535- private static long GetBlobLengthFromContentRange ( string contentRange )
539+ private static long GetBlobSize ( Response response )
536540 {
537- string size = contentRange . Split ( '/' ) [ 1 ] ;
538- return long . Parse ( size , CultureInfo . InvariantCulture ) ;
541+ if ( ! response . Headers . TryGetValue ( "Content-Range" , out string contentRange ) ||
542+ contentRange == null )
543+ {
544+ throw new RequestFailedException ( response . Status , InvalidContentRangeMessage ) ;
545+ }
546+
547+ int index = contentRange . IndexOf ( '/' ) ;
548+ if ( ! long . TryParse ( contentRange . Substring ( index + 1 ) , NumberStyles . Integer , CultureInfo . InvariantCulture , out long size ) ||
549+ size <= 0 )
550+ {
551+ throw new RequestFailedException ( response . Status , InvalidContentRangeMessage ) ;
552+ }
553+
554+ return size ;
539555 }
540556
541557 // Some streams will throw if you try to access their length so we wrap
@@ -573,26 +589,7 @@ public virtual Response<GetManifestResult> GetManifest(string tagOrDigest, Cance
573589 scope . Start ( ) ;
574590 try
575591 {
576- string accept = GetAcceptHeader ( ) ;
577-
578- Response < ManifestWrapper > response = _restClient . GetManifest ( _repositoryName , tagOrDigest , accept , cancellationToken ) ;
579- Response rawResponse = response . GetRawResponse ( ) ;
580-
581- rawResponse . Headers . TryGetValue ( "Docker-Content-Digest" , out string digest ) ;
582- rawResponse . Headers . TryGetValue ( "Content-Type" , out string contentType ) ;
583-
584- var contentDigest = BlobHelper . ComputeDigest ( rawResponse . ContentStream ) ;
585-
586- if ( ReferenceIsDigest ( tagOrDigest ) )
587- {
588- BlobHelper . ValidateDigest ( contentDigest , tagOrDigest , BlobHelper . ManifestDigestDoestMatchRequestedMessage ) ;
589- }
590- else
591- {
592- BlobHelper . ValidateDigest ( contentDigest , digest ) ;
593- }
594-
595- return Response . FromValue ( new GetManifestResult ( digest , contentType , rawResponse . Content ) , rawResponse ) ;
592+ return GetManifestInternalAsync ( tagOrDigest , false , cancellationToken ) . EnsureCompleted ( ) ;
596593 }
597594 catch ( Exception e )
598595 {
@@ -617,32 +614,39 @@ public virtual async Task<Response<GetManifestResult>> GetManifestAsync(string t
617614 scope . Start ( ) ;
618615 try
619616 {
620- string accept = GetAcceptHeader ( ) ;
617+ return await GetManifestInternalAsync ( tagOrDigest , true , cancellationToken ) . ConfigureAwait ( false ) ;
618+ }
619+ catch ( Exception e )
620+ {
621+ scope . Failed ( e ) ;
622+ throw ;
623+ }
624+ }
621625
622- Response < ManifestWrapper > response = await _restClient . GetManifestAsync ( _repositoryName , tagOrDigest , accept , cancellationToken ) . ConfigureAwait ( false ) ;
623- Response rawResponse = response . GetRawResponse ( ) ;
626+ private async Task < Response < GetManifestResult > > GetManifestInternalAsync ( string reference , bool async , CancellationToken cancellationToken )
627+ {
628+ string accept = GetAcceptHeader ( ) ;
624629
625- rawResponse . Headers . TryGetValue ( "Docker-Content-Digest" , out var digest ) ;
626- rawResponse . Headers . TryGetValue ( "Content-Type" , out string contentType ) ;
630+ Response < ManifestWrapper > response = async ?
631+ await _restClient . GetManifestAsync ( _repositoryName , reference , accept , cancellationToken ) . ConfigureAwait ( false ) :
632+ _restClient . GetManifest ( _repositoryName , reference , accept , cancellationToken ) ;
633+ Response rawResponse = response . GetRawResponse ( ) ;
627634
628- var contentDigest = BlobHelper . ComputeDigest ( rawResponse . ContentStream ) ;
635+ CheckManifestSize ( rawResponse ) ;
629636
630- if ( ReferenceIsDigest ( tagOrDigest ) )
631- {
632- BlobHelper . ValidateDigest ( contentDigest , tagOrDigest , BlobHelper . ManifestDigestDoestMatchRequestedMessage ) ;
633- }
634- else
635- {
636- BlobHelper . ValidateDigest ( contentDigest , digest ) ;
637- }
637+ rawResponse . Headers . TryGetValue ( "Docker-Content-Digest" , out string responseHeaderDigest ) ;
638+ rawResponse . Headers . TryGetValue ( "Content-Type" , out string contentType ) ;
638639
639- return Response . FromValue ( new GetManifestResult ( digest , contentType , rawResponse . Content ) , rawResponse ) ;
640- }
641- catch ( Exception e )
640+ string computedDigest = BlobHelper . ComputeDigest ( rawResponse . ContentStream ) ;
641+
642+ BlobHelper . ValidateDigest ( computedDigest , responseHeaderDigest ) ;
643+
644+ if ( ReferenceIsDigest ( reference ) )
642645 {
643- scope . Failed ( e ) ;
644- throw ;
646+ BlobHelper . ValidateDigest ( computedDigest , reference , BlobHelper . ManifestDigestDoestMatchRequestedMessage ) ;
645647 }
648+
649+ return Response . FromValue ( new GetManifestResult ( responseHeaderDigest , contentType , rawResponse . Content ) , rawResponse ) ;
646650 }
647651
648652 private static string GetAcceptHeader ( )
@@ -671,6 +675,30 @@ private static bool ReferenceIsDigest(string reference)
671675 return reference . StartsWith ( "sha256:" , StringComparison . OrdinalIgnoreCase ) ;
672676 }
673677
678+ private static void CheckContentLength ( Response response )
679+ {
680+ if ( response . Headers . ContentLength == null ||
681+ response . Headers . ContentLength <= 0 )
682+ {
683+ throw new RequestFailedException ( response . Status , InvalidContentLengthMessage ) ;
684+ }
685+ }
686+
687+ private static void CheckManifestSize ( Response response )
688+ {
689+ // This check is to address part of the service threat model.
690+ // If a manifest does not have a proper content length or is too big,
691+ // it indicates a malicious or faulty service and should not be trusted.
692+ CheckContentLength ( response ) ;
693+
694+ int ? size = response . Headers . ContentLength ;
695+
696+ if ( size > MaxManifestSize )
697+ {
698+ throw new RequestFailedException ( response . Status , "Manifest size is bigger than max allowed size of 4MB." ) ;
699+ }
700+ }
701+
674702 /// <summary>
675703 /// Download a container registry blob.
676704 /// This API is a prefered way to fetch blobs that can fit into memory.
@@ -735,14 +763,17 @@ private async Task<Response<DownloadRegistryBlobResult>> DownloadBlobContentInte
735763 await _blobRestClient . GetBlobAsync ( _repositoryName , digest , cancellationToken ) . ConfigureAwait ( false ) :
736764 _blobRestClient . GetBlob ( _repositoryName , digest , cancellationToken ) ;
737765
766+ Response response = blobResult . GetRawResponse ( ) ;
767+ CheckContentLength ( response ) ;
768+
738769 BinaryData data = async ?
739770 await BinaryData . FromStreamAsync ( blobResult . Value , cancellationToken ) . ConfigureAwait ( false ) :
740771 BinaryData . FromStream ( blobResult . Value ) ;
741772
742773 string contentDigest = BlobHelper . ComputeDigest ( data ) ;
743- BlobHelper . ValidateDigest ( contentDigest , digest ) ;
774+ BlobHelper . ValidateDigest ( contentDigest , digest , BlobHelper . ContentDigestDoesntMatchRequestedMessage ) ;
744775
745- return Response . FromValue ( new DownloadRegistryBlobResult ( digest , data ) , blobResult . GetRawResponse ( ) ) ;
776+ return Response . FromValue ( new DownloadRegistryBlobResult ( digest , data ) , response ) ;
746777 }
747778
748779 /// <summary>
@@ -837,6 +868,9 @@ private async Task<Response<DownloadRegistryBlobStreamingResult>> DownloadBlobSt
837868 await _blobRestClient . GetBlobAsync ( _repositoryName , digest , cancellationToken ) . ConfigureAwait ( false ) :
838869 _blobRestClient . GetBlob ( _repositoryName , digest , cancellationToken ) ;
839870
871+ Response response = blobResult . GetRawResponse ( ) ;
872+ CheckContentLength ( response ) ;
873+
840874 // Wrap the response Content in a RetriableStream so we
841875 // can return it before it's finished downloading, but still
842876 // allow retrying if it fails.
@@ -849,7 +883,7 @@ await _blobRestClient.GetBlobAsync(_repositoryName, digest, cancellationToken).C
849883
850884 ValidatingStream stream = new ( retriableStream , ( int ) blobResult . Headers . ContentLength . Value , digest ) ;
851885
852- return Response . FromValue ( new DownloadRegistryBlobStreamingResult ( digest , stream ) , blobResult . GetRawResponse ( ) ) ;
886+ return Response . FromValue ( new DownloadRegistryBlobStreamingResult ( digest , stream ) , response ) ;
853887 }
854888
855889 /// <summary>
@@ -988,7 +1022,7 @@ private async Task<Response> DownloadBlobToInternalAsync(string digest, Stream d
9881022 using SHA256 sha256 = SHA256 . Create ( ) ;
9891023
9901024 long blobBytes = 0 ;
991- long ? blobLength = default ;
1025+ long ? blobSize = default ;
9921026
9931027 try
9941028 {
@@ -997,16 +1031,16 @@ private async Task<Response> DownloadBlobToInternalAsync(string digest, Stream d
9971031 do
9981032 {
9991033 // Request a chunk
1000- long requestLength = blobLength . HasValue ?
1001- ( int ) Math . Min ( blobLength . Value - blobBytes , options . MaxChunkSize ) :
1034+ long requestLength = blobSize . HasValue ?
1035+ ( int ) Math . Min ( blobSize . Value - blobBytes , options . MaxChunkSize ) :
10021036 options . MaxChunkSize ;
10031037 string requestRange = new HttpRange ( blobBytes , requestLength ) . ToString ( ) ;
10041038
1005- var getChunkResponse = async ?
1039+ ResponseWithHeaders < Stream , ContainerRegistryBlobGetChunkHeaders > getChunkResponse = async ?
10061040 await _blobRestClient . GetChunkAsync ( _repositoryName , digest , requestRange , cancellationToken ) . ConfigureAwait ( false ) :
10071041 _blobRestClient . GetChunk ( _repositoryName , digest , requestRange , cancellationToken ) ;
10081042
1009- blobLength ??= GetBlobLengthFromContentRange ( getChunkResponse . Headers . ContentRange ) ;
1043+ blobSize ??= GetBlobSize ( getChunkResponse . GetRawResponse ( ) ) ;
10101044
10111045 int chunkLength = ( int ) getChunkResponse . Headers . ContentLength . Value ;
10121046 Stream responseStream = getChunkResponse . Value ;
@@ -1037,12 +1071,12 @@ await responseStream.ReadAsync(buffer, chunkBytes, chunkLength - chunkBytes, can
10371071 blobBytes += chunkBytes ;
10381072 result = getChunkResponse . GetRawResponse ( ) ;
10391073 }
1040- while ( blobBytes < blobLength . Value ) ;
1074+ while ( blobBytes < blobSize . Value ) ;
10411075
10421076 // Complete hash computation.
10431077 sha256. TransformFinalBlock ( buffer , 0 , 0 ) ;
10441078 string computedDigest = BlobHelper . FormatDigest ( sha256 . Hash ) ;
1045- BlobHelper. ValidateDigest ( computedDigest , digest ) ;
1079+ BlobHelper. ValidateDigest ( computedDigest , digest , BlobHelper . ContentDigestDoesntMatchRequestedMessage ) ;
10461080
10471081 if ( async)
10481082 {
0 commit comments