@@ -17,6 +17,7 @@ package dependencyfirewall
1717
1818import (
1919 "context"
20+ "crypto/sha256"
2021 "encoding/json"
2122 "fmt"
2223 "io"
@@ -197,6 +198,8 @@ func parseBearerChallenge(header string) (realm, service, scope string) {
197198
198199// fetchRegistryToken obtains an anonymous pull token by following the Bearer
199200// challenge advertised in the upstream 401 response.
201+ // The realm URL is validated against the upstream registry host to prevent SSRF:
202+ // a malicious registry could advertise realm="http://internal-host/" in its 401.
200203func (d * OCIDependencyProxyController ) fetchRegistryToken (ctx context.Context , wwwAuthenticate string ) (string , error ) {
201204 realm , service , scope := parseBearerChallenge (wwwAuthenticate )
202205 if realm == "" {
@@ -284,7 +287,9 @@ func (d *OCIDependencyProxyController) fetchOCIFromUpstream(ctx context.Context,
284287 }
285288
286289 defer resp .Body .Close ()
287- data , err := io .ReadAll (resp .Body )
290+ // 10 GiB ceiling — large enough for any real OCI layer, prevents unbounded memory use.
291+ const maxResponseBytes = 10 << 30
292+ data , err := io .ReadAll (io .LimitReader (resp .Body , maxResponseBytes ))
288293 if err != nil {
289294 return nil , nil , resp .StatusCode , fmt .Errorf ("failed to read response: %w" , err )
290295 }
@@ -488,6 +493,15 @@ func (d *OCIDependencyProxyController) ProxyOCIBlob(c shared.Context) error {
488493 }
489494
490495 if method == http .MethodGet && len (data ) > 0 {
496+ // Verify the downloaded content matches the requested digest before caching.
497+ // digest is of the form "sha256:<hex>"; skip verification for other algorithms.
498+ if algo , expected , ok := strings .Cut (digest , ":" ); ok && algo == "sha256" {
499+ actual := fmt .Sprintf ("%x" , sha256 .Sum256 (data ))
500+ if actual != expected {
501+ slog .Error ("OCI blob digest mismatch" , "proxy" , "oci" , "image" , fqImageName , "expected" , expected , "actual" , actual )
502+ return echo .NewHTTPError (http .StatusBadGateway , "upstream blob digest mismatch" )
503+ }
504+ }
491505 if err := d .CacheDataWithIntegrity (cachePath , data ); err != nil {
492506 slog .Warn ("Failed to cache OCI blob" , "proxy" , "oci" , "error" , err )
493507 }
@@ -504,6 +518,12 @@ func (d *OCIDependencyProxyController) ProxyOCIBlob(c shared.Context) error {
504518// - GET /v2/:registry/:image/referrers/:digest
505519// - GET /v2/:registry/:namespace/:image/referrers/:digest
506520func (d * OCIDependencyProxyController ) ProxyOCIReferrers (c shared.Context ) error {
521+ configs , err := d .GetDependencyProxyConfigs (c )
522+ if err != nil {
523+ slog .Error ("Error getting dependency proxy configs" , "error" , err )
524+ return echo .NewHTTPError (http .StatusInternalServerError , "failed to load dependency proxy configuration" )
525+ }
526+
507527 registry , fqImageName , upstreamImagePath := imageParamsFromContext (c )
508528 digest := c .Param ("digest" )
509529 upstreamPath := fmt .Sprintf ("/v2/%s/referrers/%s" , upstreamImagePath , digest )
@@ -522,6 +542,13 @@ func (d *OCIDependencyProxyController) ProxyOCIReferrers(c shared.Context) error
522542
523543 slog .Info ("Proxy request" , "proxy" , "oci" , "type" , "referrers" , "image" , fqImageName , "digest" , digest )
524544
545+ if notAllowed , reason := d .CheckNotAllowedPackage (ctx , ociEco , fqImageName , configs ); notAllowed {
546+ return d .blockNotAllowedPackage (c , ociEco , fqImageName , reason )
547+ }
548+ if blocked , reason := d .checkMaliciousPackage (ctx , ociEco , fqImageName ); blocked {
549+ return d .blockMaliciousPackage (c , ociEco , fqImageName , reason )
550+ }
551+
525552 upstreamBase := upstreamURLForRegistry (registry )
526553 data , headers , statusCode , err := d .fetchOCIFromUpstream (ctx , c .Request ().Method , upstreamBase , upstreamPath )
527554 if err != nil {
@@ -538,6 +565,12 @@ func (d *OCIDependencyProxyController) ProxyOCIReferrers(c shared.Context) error
538565// - GET /oci/v2/:registry/:image/tags/list
539566// - GET /oci/v2/:registry/:namespace/:image/tags/list
540567func (d * OCIDependencyProxyController ) ProxyOCITagsList (c shared.Context ) error {
568+ configs , err := d .GetDependencyProxyConfigs (c )
569+ if err != nil {
570+ slog .Error ("Error getting dependency proxy configs" , "error" , err )
571+ return echo .NewHTTPError (http .StatusInternalServerError , "failed to load dependency proxy configuration" )
572+ }
573+
541574 registry , fqImageName , upstreamImagePath := imageParamsFromContext (c )
542575 upstreamPath := fmt .Sprintf ("/v2/%s/tags/list" , upstreamImagePath )
543576
@@ -554,6 +587,13 @@ func (d *OCIDependencyProxyController) ProxyOCITagsList(c shared.Context) error
554587
555588 slog .Info ("Proxy request" , "proxy" , "oci" , "type" , "tags/list" , "image" , fqImageName )
556589
590+ if notAllowed , reason := d .CheckNotAllowedPackage (ctx , ociEco , fqImageName , configs ); notAllowed {
591+ return d .blockNotAllowedPackage (c , ociEco , fqImageName , reason )
592+ }
593+ if blocked , reason := d .checkMaliciousPackage (ctx , ociEco , fqImageName ); blocked {
594+ return d .blockMaliciousPackage (c , ociEco , fqImageName , reason )
595+ }
596+
557597 upstreamBase := upstreamURLForRegistry (registry )
558598 data , headers , statusCode , err := d .fetchOCIFromUpstream (ctx , c .Request ().Method , upstreamBase , upstreamPath )
559599 if err != nil {
0 commit comments