diff --git a/cmd/oras/internal/option/packer.go b/cmd/oras/internal/option/packer.go index 7fe058b83..d32fa62de 100644 --- a/cmd/oras/internal/option/packer.go +++ b/cmd/oras/internal/option/packer.go @@ -21,11 +21,13 @@ import ( "errors" "fmt" "os" + "path/filepath" "strings" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/pflag" "oras.land/oras-go/v2/content" + "oras.land/oras/cmd/oras/internal/fileref" ) // Pre-defined annotation keys for annotation file @@ -38,6 +40,7 @@ var ( errAnnotationConflict = errors.New("`--annotation` and `--annotation-file` cannot be both specified") errAnnotationFormat = errors.New("missing key in `--annotation` flag") errAnnotationDuplication = errors.New("duplicate annotation key") + errPathValidation = errors.New("absolute file path detected. If it's intentional, use --disable-path-validation flag to skip this check") ) // Packer option struct. @@ -69,6 +72,25 @@ func (opts *Packer) ExportManifest(ctx context.Context, fetcher content.Fetcher, } return os.WriteFile(opts.ManifestExportPath, manifestBytes, 0666) } +func (opts *Packer) Parse() error { + if !opts.PathValidationDisabled { + var failedPaths []string + for _, path := range opts.FileRefs { + // Remove the type if specified in the path [:] format + path, _, err := fileref.Parse(path, "") + if err != nil { + return err + } + if filepath.IsAbs(path) { + failedPaths = append(failedPaths, path) + } + } + if len(failedPaths) > 0 { + return fmt.Errorf("%w: %v", errPathValidation, strings.Join(failedPaths, ", ")) + } + } + return nil +} // LoadManifestAnnotations loads the manifest annotation map. func (opts *Packer) LoadManifestAnnotations() (annotations map[string]map[string]string, err error) { diff --git a/cmd/oras/root/attach.go b/cmd/oras/root/attach.go index 3d0d07eee..e5f858695 100644 --- a/cmd/oras/root/attach.go +++ b/cmd/oras/root/attach.go @@ -116,7 +116,6 @@ func runAttach(ctx context.Context, opts attachOptions) error { return err } defer store.Close() - store.AllowPathTraversalOnWrite = opts.PathValidationDisabled dst, err := opts.NewTarget(opts.Common) if err != nil { diff --git a/cmd/oras/root/pull.go b/cmd/oras/root/pull.go index ec25da1c0..703833d90 100644 --- a/cmd/oras/root/pull.go +++ b/cmd/oras/root/pull.go @@ -17,6 +17,7 @@ package root import ( "context" + "errors" "fmt" "io" "sync" @@ -237,6 +238,9 @@ func runPull(ctx context.Context, opts pullOptions) error { // Copy desc, err := oras.Copy(ctx, src, opts.Reference, dst, opts.Reference, copyOptions) if err != nil { + if errors.Is(err, file.ErrPathTraversalDisallowed) { + err = fmt.Errorf("%s: %w", "use flag --allow-path-traversal to allow insecurely pulling files outside of working directory", err) + } return err } if pulledEmpty { diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index 152a2b2e1..698d3e57d 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -139,7 +139,6 @@ func runPush(ctx context.Context, opts pushOptions) error { return err } defer store.Close() - store.AllowPathTraversalOnWrite = opts.PathValidationDisabled if opts.manifestConfigRef != "" { path, cfgMediaType, err := fileref.Parse(opts.manifestConfigRef, oras.MediaTypeUnknownConfig) if err != nil { diff --git a/cmd/oras/root/repo/tags.go b/cmd/oras/root/repo/tags.go index 21435eadf..490a0fc94 100644 --- a/cmd/oras/root/repo/tags.go +++ b/cmd/oras/root/repo/tags.go @@ -55,10 +55,10 @@ Example - Show tags of the target OCI layout folder 'layout-dir': Example - Show tags of the target OCI layout archive 'layout.tar': oras repo tags --oci-layout layout.tar -Example - Show tags associated with a particular tagged resource: +Example - [Experimental] Show tags associated with a particular tagged resource: oras repo tags localhost:5000/hello:latest -Example - Show tags associated with a digest: +Example - [Experimental] Show tags associated with a digest: oras repo tags localhost:5000/hello@sha256:c551125a624189cece9135981621f3f3144564ddabe14b523507bf74c2281d9b `, Args: cobra.ExactArgs(1), @@ -95,7 +95,7 @@ func showTags(ctx context.Context, opts showTagsOptions) error { } filter = desc.Digest.String() } - logger.Infof("[Experimental] querying tags associated to %s, it may take a while...\n", filter) + logger.Warnf("[Experimental] querying tags associated to %s, it may take a while...\n", filter) } return finder.Tags(ctx, opts.last, func(tags []string) error { for _, tag := range tags { diff --git a/test/e2e/internal/testdata/multi_arch/const.go b/test/e2e/internal/testdata/multi_arch/const.go index 4bf2da2a8..a90e8e2e3 100644 --- a/test/e2e/internal/testdata/multi_arch/const.go +++ b/test/e2e/internal/testdata/multi_arch/const.go @@ -63,8 +63,8 @@ var ( LinuxAMD64ReferrerStateKey = match.StateKey{Digest: "57e6462826c8", Name: "application/vnd.oci.image.manifest.v1+json"} LinuxAMD64ReferrerConfigStateKey = match.StateKey{Digest: "44136fa355b3", Name: "referrer.image"} LinuxAMD64StateKeys = []match.StateKey{ - {Digest: "9d84a5716c66", Name: ocispec.MediaTypeImageManifest}, - {Digest: "fe9dbc99451d", Name: ocispec.MediaTypeImageConfig}, + {Digest: "9d84a5716c66", Name: "application/vnd.oci.image.manifest.v1+json"}, + {Digest: "fe9dbc99451d", Name: "application/vnd.oci.image.config.v1+json"}, {Digest: "2ef548696ac7", Name: "hello.tar"}, } ) diff --git a/test/e2e/suite/command/attach.go b/test/e2e/suite/command/attach.go index 1410bbed6..499a613da 100644 --- a/test/e2e/suite/command/attach.go +++ b/test/e2e/suite/command/attach.go @@ -104,6 +104,7 @@ var _ = Describe("Common registry users:", func() { fetched := ORAS("manifest", "fetch", RegistryRef(Host, testRepo, index.Manifests[0].Digest.String())).Exec().Out.Contents() MatchFile(filepath.Join(tempDir, exportName), string(fetched), DefaultTimeout) }) + It("should attach a file via a OCI Image", func() { testRepo := attachTestRepo("image") tempDir := PrepareTempFiles() @@ -119,8 +120,35 @@ var _ = Describe("Common registry users:", func() { bytes := ORAS("discover", subjectRef, "-o", "json").Exec().Out.Contents() Expect(json.Unmarshal(bytes, &index)).ShouldNot(HaveOccurred()) Expect(len(index.Manifests)).To(Equal(1)) - Expect(index.Manifests[0].MediaType).To(Equal(ocispec.MediaTypeImageManifest)) + Expect(index.Manifests[0].MediaType).To(Equal("application/vnd.oci.image.manifest.v1+json")) + }) + + It("should attach file with path validation disabled", func() { + testRepo := attachTestRepo("simple") + absAttachFileName := filepath.Join(PrepareTempFiles(), foobar.AttachFileName) + + subjectRef := RegistryRef(Host, testRepo, foobar.Tag) + prepare(RegistryRef(Host, ImageRepo, foobar.Tag), subjectRef) + statusKey := foobar.AttachFileStateKey + statusKey.Name = absAttachFileName + ORAS("attach", "--artifact-type", "test.attach", subjectRef, fmt.Sprintf("%s:%s", absAttachFileName, foobar.AttachFileMedia), "--disable-path-validation"). + MatchStatus([]match.StateKey{statusKey}, false, 1). + Exec() + }) + + It("should fail path validation when attaching file with absolute path", func() { + testRepo := attachTestRepo("simple") + absAttachFileName := filepath.Join(PrepareTempFiles(), foobar.AttachFileName) + + subjectRef := RegistryRef(Host, testRepo, foobar.Tag) + prepare(RegistryRef(Host, ImageRepo, foobar.Tag), subjectRef) + statusKey := foobar.AttachFileStateKey + statusKey.Name = absAttachFileName + ORAS("attach", "--artifact-type", "test.attach", subjectRef, fmt.Sprintf("%s:%s", absAttachFileName, foobar.AttachFileMedia)). + ExpectFailure(). + Exec() }) + It("should attach a file via a OCI Artifact", func() { testRepo := attachTestRepo("artifact") tempDir := PrepareTempFiles() @@ -136,7 +164,7 @@ var _ = Describe("Common registry users:", func() { bytes := ORAS("discover", subjectRef, "-o", "json").Exec().Out.Contents() Expect(json.Unmarshal(bytes, &index)).ShouldNot(HaveOccurred()) Expect(len(index.Manifests)).To(Equal(1)) - Expect(index.Manifests[0].MediaType).To(Equal(ocispec.MediaTypeArtifactManifest)) + Expect(index.Manifests[0].MediaType).To(Equal("application/vnd.oci.artifact.manifest.v1+json")) }) }) }) @@ -158,7 +186,7 @@ var _ = Describe("Fallback registry users:", func() { bytes := ORAS("discover", subjectRef, "-o", "json").Exec().Out.Contents() Expect(json.Unmarshal(bytes, &index)).ShouldNot(HaveOccurred()) Expect(len(index.Manifests)).To(Equal(1)) - Expect(index.Manifests[0].MediaType).To(Equal(ocispec.MediaTypeImageManifest)) + Expect(index.Manifests[0].MediaType).To(Equal("application/vnd.oci.image.manifest.v1+json")) }) It("should attach a file via a OCI Image by default", func() { @@ -176,7 +204,7 @@ var _ = Describe("Fallback registry users:", func() { bytes := ORAS("discover", subjectRef, "-o", "json").Exec().Out.Contents() Expect(json.Unmarshal(bytes, &index)).ShouldNot(HaveOccurred()) Expect(len(index.Manifests)).To(Equal(1)) - Expect(index.Manifests[0].MediaType).To(Equal(ocispec.MediaTypeImageManifest)) + Expect(index.Manifests[0].MediaType).To(Equal("application/vnd.oci.image.manifest.v1+json")) }) It("should fail to attach again when cleaning referrers index", func() { @@ -241,7 +269,7 @@ var _ = Describe("Fallback registry users:", func() { bytes := ORAS("discover", subjectRef, "--distribution-spec", "v1.1-referrers-tag", "-o", "json").Exec().Out.Contents() Expect(json.Unmarshal(bytes, &index)).ShouldNot(HaveOccurred()) Expect(len(index.Manifests)).To(Equal(1)) - Expect(index.Manifests[0].MediaType).To(Equal(ocispec.MediaTypeImageManifest)) + Expect(index.Manifests[0].MediaType).To(Equal("application/vnd.oci.image.manifest.v1+json")) }) }) }) @@ -286,7 +314,7 @@ var _ = Describe("OCI image layout users:", func() { bytes := ORAS("discover", Flags.Layout, subjectRef, "-o", "json").Exec().Out.Contents() Expect(json.Unmarshal(bytes, &index)).ShouldNot(HaveOccurred()) Expect(len(index.Manifests)).To(Equal(1)) - Expect(index.Manifests[0].MediaType).To(Equal(ocispec.MediaTypeImageManifest)) + Expect(index.Manifests[0].MediaType).To(Equal("application/vnd.oci.image.manifest.v1+json")) fetched := ORAS("manifest", "fetch", Flags.Layout, LayoutRef(root, index.Manifests[0].Digest.String())).Exec().Out.Contents() MatchFile(filepath.Join(root, exportName), string(fetched), DefaultTimeout) }) @@ -304,7 +332,7 @@ var _ = Describe("OCI image layout users:", func() { bytes := ORAS("discover", subjectRef, Flags.Layout, "-o", "json").Exec().Out.Contents() Expect(json.Unmarshal(bytes, &index)).ShouldNot(HaveOccurred()) Expect(len(index.Manifests)).To(Equal(1)) - Expect(index.Manifests[0].MediaType).To(Equal(ocispec.MediaTypeImageManifest)) + Expect(index.Manifests[0].MediaType).To(Equal("application/vnd.oci.image.manifest.v1+json")) }) It("should attach a file via a OCI Artifact", func() { root := PrepareTempFiles() @@ -320,7 +348,7 @@ var _ = Describe("OCI image layout users:", func() { bytes := ORAS("discover", subjectRef, Flags.Layout, "-o", "json").Exec().Out.Contents() Expect(json.Unmarshal(bytes, &index)).ShouldNot(HaveOccurred()) Expect(len(index.Manifests)).To(Equal(1)) - Expect(index.Manifests[0].MediaType).To(Equal(ocispec.MediaTypeArtifactManifest)) + Expect(index.Manifests[0].MediaType).To(Equal("application/vnd.oci.artifact.manifest.v1+json")) }) }) }) diff --git a/test/e2e/suite/command/manifest.go b/test/e2e/suite/command/manifest.go index effd932e5..36d30bf0c 100644 --- a/test/e2e/suite/command/manifest.go +++ b/test/e2e/suite/command/manifest.go @@ -299,7 +299,7 @@ var _ = Describe("Common registry users:", func() { It("should push a manifest from file", func() { manifestPath := WriteTempFile("manifest.json", manifest) tag := "from-file" - ORAS("manifest", "push", RegistryRef(Host, ImageRepo, tag), manifestPath, "--media-type", ocispec.MediaTypeImageManifest). + ORAS("manifest", "push", RegistryRef(Host, ImageRepo, tag), manifestPath, "--media-type", "application/vnd.oci.image.manifest.v1+json"). MatchKeyWords("Pushed", RegistryRef(Host, ImageRepo, tag), "Digest:", digest). WithInput(strings.NewReader(manifest)).Exec() }) @@ -308,7 +308,7 @@ var _ = Describe("Common registry users:", func() { manifest := `{"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:fe9dbc99451d0517d65e048c309f0b5afb2cc513b7a3d456b6cc29fe641386c5","size":53}}` digest := "sha256:0c2ae2c73c5dde0a42582d328b2e2ea43f36ba20f604fa8706f441ac8b0a3445" tag := "mediatype-flag" - ORAS("manifest", "push", RegistryRef(Host, ImageRepo, tag), "-", "--media-type", ocispec.MediaTypeImageManifest). + ORAS("manifest", "push", RegistryRef(Host, ImageRepo, tag), "-", "--media-type", "application/vnd.oci.image.manifest.v1+json"). MatchKeyWords("Pushed", RegistryRef(Host, ImageRepo, tag), "Digest:", digest). WithInput(strings.NewReader(manifest)).Exec() diff --git a/test/e2e/suite/command/push.go b/test/e2e/suite/command/push.go index dfa56e283..17139fed4 100644 --- a/test/e2e/suite/command/push.go +++ b/test/e2e/suite/command/push.go @@ -25,6 +25,7 @@ import ( "github.com/onsi/gomega" . "github.com/onsi/gomega" "github.com/onsi/gomega/gbytes" + "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/test/e2e/internal/testdata/feature" "oras.land/oras/test/e2e/internal/testdata/foobar" @@ -65,6 +66,39 @@ var _ = Describe("Remote registry users:", func() { Expect(manifest.Layers).Should(ContainElements(foobar.BlobBarDescriptor("application/vnd.oci.image.layer.v1.tar"))) }) + It("should push files with path validation disabled", func() { + repo := fmt.Sprintf("%s/%s", repoPrefix, "disable-path-validation") + ref := RegistryRef(Host, repo, tag) + absBarName := filepath.Join(PrepareTempFiles(), foobar.FileBarName) + + ORAS("push", ref, absBarName, "-v", "--disable-path-validation"). + Exec() + + // validate + fetched := ORAS("manifest", "fetch", ref).Exec().Out.Contents() + var manifest ocispec.Manifest + Expect(json.Unmarshal(fetched, &manifest)).ShouldNot(HaveOccurred()) + Expect(manifest.Layers).Should(ContainElements(ocispec.Descriptor{ + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: digest.Digest(foobar.BarBlobDigest), + Size: 3, + Annotations: map[string]string{ + "org.opencontainers.image.title": absBarName, + }, + })) + }) + + It("should fail path validation when pushing file with absolute path", func() { + repo := fmt.Sprintf("%s/%s", repoPrefix, "path-validation") + ref := RegistryRef(Host, repo, tag) + absBarName := filepath.Join(PrepareTempFiles(), foobar.FileBarName) + // test + ORAS("push", ref, absBarName, "-v"). + MatchErrKeyWords("--disable-path-validation"). + ExpectFailure(). + Exec() + }) + It("should push files and tag", func() { repo := fmt.Sprintf("%s/%s", repoPrefix, "multi-tag") tempDir := PrepareTempFiles()