@@ -20,6 +20,10 @@ namespace Azure.Containers.ContainerRegistry
20
20
public class ContainerRegistryContentClient
21
21
{
22
22
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." ;
23
27
24
28
private readonly Uri _endpoint ;
25
29
private readonly string _registryName ;
@@ -532,10 +536,22 @@ private static string GetContentRange(long offset, long length)
532
536
return FormattableString . Invariant ( $ "{ offset } -{ endRange } ") ;
533
537
}
534
538
535
- private static long GetBlobLengthFromContentRange ( string contentRange )
539
+ private static long GetBlobSize ( Response response )
536
540
{
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 ;
539
555
}
540
556
541
557
// 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
573
589
scope . Start ( ) ;
574
590
try
575
591
{
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 ( ) ;
596
593
}
597
594
catch ( Exception e )
598
595
{
@@ -617,32 +614,39 @@ public virtual async Task<Response<GetManifestResult>> GetManifestAsync(string t
617
614
scope . Start ( ) ;
618
615
try
619
616
{
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
+ }
621
625
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 ( ) ;
624
629
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 ( ) ;
627
634
628
- var contentDigest = BlobHelper . ComputeDigest ( rawResponse . ContentStream ) ;
635
+ CheckManifestSize ( rawResponse ) ;
629
636
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 ) ;
638
639
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 ) )
642
645
{
643
- scope . Failed ( e ) ;
644
- throw ;
646
+ BlobHelper . ValidateDigest ( computedDigest , reference , BlobHelper . ManifestDigestDoestMatchRequestedMessage ) ;
645
647
}
648
+
649
+ return Response . FromValue ( new GetManifestResult ( responseHeaderDigest , contentType , rawResponse . Content ) , rawResponse ) ;
646
650
}
647
651
648
652
private static string GetAcceptHeader ( )
@@ -671,6 +675,30 @@ private static bool ReferenceIsDigest(string reference)
671
675
return reference . StartsWith ( "sha256:" , StringComparison . OrdinalIgnoreCase ) ;
672
676
}
673
677
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
+
674
702
/// <summary>
675
703
/// Download a container registry blob.
676
704
/// This API is a prefered way to fetch blobs that can fit into memory.
@@ -735,14 +763,17 @@ private async Task<Response<DownloadRegistryBlobResult>> DownloadBlobContentInte
735
763
await _blobRestClient . GetBlobAsync ( _repositoryName , digest , cancellationToken ) . ConfigureAwait ( false ) :
736
764
_blobRestClient . GetBlob ( _repositoryName , digest , cancellationToken ) ;
737
765
766
+ Response response = blobResult . GetRawResponse ( ) ;
767
+ CheckContentLength ( response ) ;
768
+
738
769
BinaryData data = async ?
739
770
await BinaryData . FromStreamAsync ( blobResult . Value , cancellationToken ) . ConfigureAwait ( false ) :
740
771
BinaryData . FromStream ( blobResult . Value ) ;
741
772
742
773
string contentDigest = BlobHelper . ComputeDigest ( data ) ;
743
- BlobHelper . ValidateDigest ( contentDigest , digest ) ;
774
+ BlobHelper . ValidateDigest ( contentDigest , digest , BlobHelper . ContentDigestDoesntMatchRequestedMessage ) ;
744
775
745
- return Response . FromValue ( new DownloadRegistryBlobResult ( digest , data ) , blobResult . GetRawResponse ( ) ) ;
776
+ return Response . FromValue ( new DownloadRegistryBlobResult ( digest , data ) , response ) ;
746
777
}
747
778
748
779
/// <summary>
@@ -837,6 +868,9 @@ private async Task<Response<DownloadRegistryBlobStreamingResult>> DownloadBlobSt
837
868
await _blobRestClient . GetBlobAsync ( _repositoryName , digest , cancellationToken ) . ConfigureAwait ( false ) :
838
869
_blobRestClient . GetBlob ( _repositoryName , digest , cancellationToken ) ;
839
870
871
+ Response response = blobResult . GetRawResponse ( ) ;
872
+ CheckContentLength ( response ) ;
873
+
840
874
// Wrap the response Content in a RetriableStream so we
841
875
// can return it before it's finished downloading, but still
842
876
// allow retrying if it fails.
@@ -849,7 +883,7 @@ await _blobRestClient.GetBlobAsync(_repositoryName, digest, cancellationToken).C
849
883
850
884
ValidatingStream stream = new ( retriableStream , ( int ) blobResult . Headers . ContentLength . Value , digest ) ;
851
885
852
- return Response . FromValue ( new DownloadRegistryBlobStreamingResult ( digest , stream ) , blobResult . GetRawResponse ( ) ) ;
886
+ return Response . FromValue ( new DownloadRegistryBlobStreamingResult ( digest , stream ) , response ) ;
853
887
}
854
888
855
889
/// <summary>
@@ -988,7 +1022,7 @@ private async Task<Response> DownloadBlobToInternalAsync(string digest, Stream d
988
1022
using SHA256 sha256 = SHA256 . Create ( ) ;
989
1023
990
1024
long blobBytes = 0 ;
991
- long ? blobLength = default ;
1025
+ long ? blobSize = default ;
992
1026
993
1027
try
994
1028
{
@@ -997,16 +1031,16 @@ private async Task<Response> DownloadBlobToInternalAsync(string digest, Stream d
997
1031
do
998
1032
{
999
1033
// 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 ) :
1002
1036
options . MaxChunkSize ;
1003
1037
string requestRange = new HttpRange ( blobBytes , requestLength ) . ToString ( ) ;
1004
1038
1005
- var getChunkResponse = async ?
1039
+ ResponseWithHeaders < Stream , ContainerRegistryBlobGetChunkHeaders > getChunkResponse = async ?
1006
1040
await _blobRestClient . GetChunkAsync ( _repositoryName , digest , requestRange , cancellationToken ) . ConfigureAwait ( false ) :
1007
1041
_blobRestClient . GetChunk ( _repositoryName , digest , requestRange , cancellationToken ) ;
1008
1042
1009
- blobLength ??= GetBlobLengthFromContentRange ( getChunkResponse . Headers . ContentRange ) ;
1043
+ blobSize ??= GetBlobSize ( getChunkResponse . GetRawResponse ( ) ) ;
1010
1044
1011
1045
int chunkLength = ( int ) getChunkResponse . Headers . ContentLength . Value ;
1012
1046
Stream responseStream = getChunkResponse . Value ;
@@ -1037,12 +1071,12 @@ await responseStream.ReadAsync(buffer, chunkBytes, chunkLength - chunkBytes, can
1037
1071
blobBytes += chunkBytes ;
1038
1072
result = getChunkResponse . GetRawResponse ( ) ;
1039
1073
}
1040
- while ( blobBytes < blobLength . Value ) ;
1074
+ while ( blobBytes < blobSize . Value ) ;
1041
1075
1042
1076
// Complete hash computation.
1043
1077
sha256. TransformFinalBlock ( buffer , 0 , 0 ) ;
1044
1078
string computedDigest = BlobHelper . FormatDigest ( sha256 . Hash ) ;
1045
- BlobHelper. ValidateDigest ( computedDigest , digest ) ;
1079
+ BlobHelper. ValidateDigest ( computedDigest , digest , BlobHelper . ContentDigestDoesntMatchRequestedMessage ) ;
1046
1080
1047
1081
if ( async)
1048
1082
{
0 commit comments