github.com/opencontainers/umoci@v0.4.8-0.20240508124516-656e4836fb0d/oci/casext/mediatype/parse.go (about)

     1  /*
     2   * umoci: Umoci Modifies Open Containers' Images
     3   * Copyright (C) 2016-2020 SUSE LLC
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *    http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17  
    18  package mediatype
    19  
    20  import (
    21  	"encoding/json"
    22  	"io"
    23  	"reflect"
    24  	"sync"
    25  
    26  	ispec "github.com/opencontainers/image-spec/specs-go/v1"
    27  	"github.com/pkg/errors"
    28  )
    29  
    30  // ParseFunc is a parser that is registered for a given mediatype and called
    31  // to parse a blob if it is encountered. If possible, the blob should be
    32  // represented as a native Go object (with all Descriptors represented as
    33  // ispec.Descriptor objects) -- this will allow umoci to recursively discover
    34  // blob dependencies.
    35  //
    36  // Currently, we require the returned interface{} to be a raw struct
    37  // (unexpected behaviour may occur otherwise).
    38  //
    39  // NOTE: Your ParseFunc must be able to accept a nil Reader (the error
    40  //
    41  //	value is not relevant). This is used during registration in order to
    42  //	determine the type of the struct (thus you must return a struct that
    43  //	you would return in a non-nil reader scenario). Go doesn't have a way
    44  //	for us to enforce this.
    45  type ParseFunc func(io.Reader) (interface{}, error)
    46  
    47  var (
    48  	lock sync.RWMutex
    49  
    50  	// parsers is a mapping of media-type to parser function.
    51  	parsers = map[string]ParseFunc{}
    52  
    53  	// packages is the set of package paths which have been registered.
    54  	packages = map[string]struct{}{}
    55  
    56  	// targets is the set of media-types which are treated as "targets" for the
    57  	// purposes of reference resolution (resolution terminates at these targets
    58  	// as well as any un-parseable blob types).
    59  	targets = map[string]struct{}{}
    60  )
    61  
    62  // IsRegisteredPackage returns whether a parser which returns a type from the
    63  // given package path was registered. This is only useful to allow restricting
    64  // reflection recursion (as a first-pass to limit how deep reflection goes).
    65  func IsRegisteredPackage(pkgPath string) bool {
    66  	lock.RLock()
    67  	_, ok := packages[pkgPath]
    68  	lock.RUnlock()
    69  	return ok
    70  }
    71  
    72  // GetParser returns the ParseFunc that was previously registered for the given
    73  // media-type with RegisterParser (or nil if the media-type is unknown).
    74  func GetParser(mediaType string) ParseFunc {
    75  	lock.RLock()
    76  	fn := parsers[mediaType]
    77  	lock.RUnlock()
    78  	return fn
    79  }
    80  
    81  // RegisterParser registers a new ParseFunc to be used when the given
    82  // media-type is encountered during parsing or recursive walks of blobs. See
    83  // the documentation of ParseFunc for more detail.
    84  func RegisterParser(mediaType string, parser ParseFunc) {
    85  	// Get the return type so we know what packages are white-listed for
    86  	// recursion. #nosec G104
    87  	v, _ := parser(nil)
    88  	t := reflect.TypeOf(v)
    89  
    90  	// Register the parser and package.
    91  	lock.Lock()
    92  	_, old := parsers[mediaType]
    93  	parsers[mediaType] = parser
    94  	packages[t.PkgPath()] = struct{}{}
    95  	lock.Unlock()
    96  
    97  	// This should never happen, and is a programmer bug.
    98  	if old {
    99  		panic("RegisterParser() called with already-registered media-type: " + mediaType)
   100  	}
   101  }
   102  
   103  // IsTarget returns whether the given media-type should be treated as a "target
   104  // media-type" for the purposes of reference resolution. This means that either
   105  // the media-type has been registered as a target (using RegisterTarget) or has
   106  // not been registered as parseable (using RegisterParser).
   107  func IsTarget(mediaType string) bool {
   108  	lock.RLock()
   109  	_, isParseable := parsers[mediaType]
   110  	_, isTarget := targets[mediaType]
   111  	lock.RUnlock()
   112  	return isTarget || !isParseable
   113  }
   114  
   115  // RegisterTarget registers that a given *parseable* media-type (meaning that
   116  // there is a parser already registered using RegisterParser) should be treated
   117  // as a "target" for the purposes of reference resolution. This means that if
   118  // this media-type is encountered during a reference resolution walk, a
   119  // DescriptorPath to *that* blob will be returned and resolution will not
   120  // recurse any deeper. All un-parseable blobs are treated as targets, so this
   121  // is only useful for blobs that have also been given parsers.
   122  func RegisterTarget(mediaType string) {
   123  	lock.Lock()
   124  	targets[mediaType] = struct{}{}
   125  	lock.Unlock()
   126  }
   127  
   128  // CustomJSONParser creates a custom ParseFunc which JSON-decodes blob data
   129  // into the type of the given value (which *must* be a struct, otherwise
   130  // CustomJSONParser will panic). This is intended to make ergonomic use of
   131  // RegisterParser much simpler.
   132  func CustomJSONParser(v interface{}) ParseFunc {
   133  	t := reflect.TypeOf(v)
   134  	// These should never happen and are programmer bugs.
   135  	if t == nil {
   136  		panic("CustomJSONParser() called with nil interface!")
   137  	}
   138  	if t.Kind() != reflect.Struct {
   139  		panic("CustomJSONParser() called with non-struct kind!")
   140  	}
   141  	return func(rdr io.Reader) (_ interface{}, err error) {
   142  		ptr := reflect.New(t)
   143  		if rdr != nil {
   144  			err = json.NewDecoder(rdr).Decode(ptr.Interface())
   145  		}
   146  		ret := reflect.Indirect(ptr)
   147  		return ret.Interface(), err
   148  	}
   149  }
   150  
   151  func indexParser(rdr io.Reader) (interface{}, error) {
   152  	// Construct a fake struct which contains bad fields. CVE-2021-41190
   153  	var index struct {
   154  		ispec.Index
   155  		Config json.RawMessage `json:"config,omitempty"`
   156  		Layers json.RawMessage `json:"layers,omitempty"`
   157  	}
   158  	if rdr != nil {
   159  		if err := json.NewDecoder(rdr).Decode(&index); err != nil {
   160  			return nil, err
   161  		}
   162  	}
   163  	if index.MediaType != "" && index.MediaType != ispec.MediaTypeImageIndex {
   164  		return nil, errors.Errorf("malicious image detected: index contained incorrect mediaType: %s", index.MediaType)
   165  	}
   166  	if len(index.Config) != 0 {
   167  		return nil, errors.New("malicious image detected: index contained forbidden 'config' field")
   168  	}
   169  	if len(index.Layers) != 0 {
   170  		return nil, errors.New("malicious image detected: index contained forbidden 'layers' field")
   171  	}
   172  	return index.Index, nil
   173  }
   174  
   175  func manifestParser(rdr io.Reader) (interface{}, error) {
   176  	// Construct a fake struct which contains bad fields. CVE-2021-41190
   177  	var manifest struct {
   178  		ispec.Manifest
   179  		Manifests json.RawMessage `json:"manifests,omitempty"`
   180  	}
   181  	if rdr != nil {
   182  		if err := json.NewDecoder(rdr).Decode(&manifest); err != nil {
   183  			return nil, err
   184  		}
   185  	}
   186  	if manifest.MediaType != "" && manifest.MediaType != ispec.MediaTypeImageManifest {
   187  		return nil, errors.Errorf("malicious manifest detected: manifest contained incorrect mediaType: %s", manifest.MediaType)
   188  	}
   189  	if len(manifest.Manifests) != 0 {
   190  		return nil, errors.New("malicious manifest detected: manifest contained forbidden 'manifests' field")
   191  	}
   192  	return manifest.Manifest, nil
   193  }
   194  
   195  // Register the core image-spec types.
   196  func init() {
   197  	RegisterParser(ispec.MediaTypeDescriptor, CustomJSONParser(ispec.Descriptor{}))
   198  	RegisterParser(ispec.MediaTypeImageIndex, indexParser)
   199  	RegisterParser(ispec.MediaTypeImageConfig, CustomJSONParser(ispec.Image{}))
   200  
   201  	RegisterTarget(ispec.MediaTypeImageManifest)
   202  	RegisterParser(ispec.MediaTypeImageManifest, manifestParser)
   203  }