Files
container/Tests/ContainerizationOCITests/DescriptorDecodingTests.swift
T
c0185aea5c Improve security across containerization (#898)
This PR fixes a number of issues: 
- validate descriptor digests before filesystem use
- validate container and exec ids before building paths in
ManagedContainer
- Check for attribute length when consuming extended attributes in
ContainerizationEXT4
- Check directory name length when formatting an ext4 fs
- Prevent OCI image layout load from reading host files through a
symlink
- Validate registry auth realm before exchanging credentials

---------

Signed-off-by: Kathryn Baldauf <k_baldauf@apple.com>
Co-authored-by: John Logan <john_logan@apple.com>
Co-authored-by: Raj Aryan Singh <rajaryan_singh@apple.com>
Co-authored-by: Raj <realrajaryan@gmail.com>
2026-08-28 13:22:06 -07:00

137 lines
5.4 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import Foundation
import Testing
@testable import ContainerizationOCI
/// Registry and OCI layout JSON is the trust boundary for descriptor digests.
/// Rejecting a malformed digest here is what stops one from being written into
/// the local store and dereferenced later.
@Suite
struct DescriptorDecodingTests {
private static let valid = "sha256:\(String(repeating: "a", count: 64))"
private static let traversal = "sha256:../../../../etc/hosts"
private func decode<T: Decodable>(_ type: T.Type, _ json: String) throws -> T {
let data = try #require(json.data(using: .utf8))
return try JSONDecoder().decode(type, from: data)
}
@Test func acceptsCanonicalDescriptor() throws {
let json = """
{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"\(Self.valid)","size":1234}
"""
let decoded = try decode(Descriptor.self, json)
#expect(decoded.digest == Self.valid)
#expect(decoded.size == 1234)
}
@Test(arguments: [
"sha256:../../../../etc/hosts",
"../../etc/hosts",
"sha256:",
"",
"sha256:\(String(repeating: "a", count: 63))",
"sha256:\(String(repeating: "A", count: 64))",
"sha512:\(String(repeating: "a", count: 128))",
"\(String(repeating: "a", count: 64))",
])
func rejectsMalformedDescriptorDigest(_ digest: String) throws {
let json = """
{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"\(digest)","size":1}
"""
#expect(throws: DecodingError.self) {
try decode(Descriptor.self, json)
}
}
@Test func rejectsNegativeSize() throws {
let json = """
{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"\(Self.valid)","size":-1}
"""
// A negative size flips the small/large blob branch during import and
// corrupts progress accounting.
#expect(throws: DecodingError.self) {
try decode(Descriptor.self, json)
}
}
@Test func rejectsIndexWithTraversingChild() throws {
// This is the shape that matters: the index itself is content addressed
// and verifiable, but a child entry for a platform the caller did not
// ask for is never fetched, so nothing else would ever check it.
let json = """
{
"schemaVersion": 2,
"manifests": [
{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"\(Self.valid)","size":10,
"platform":{"architecture":"arm64","os":"linux"}},
{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"\(Self.traversal)","size":10,
"platform":{"architecture":"ppc64le","os":"linux"}}
]
}
"""
#expect(throws: DecodingError.self) {
try decode(Index.self, json)
}
}
@Test func rejectsIndexWithTraversingSubject() throws {
let json = """
{"schemaVersion":2,"manifests":[],
"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"\(Self.traversal)","size":1}}
"""
#expect(throws: DecodingError.self) {
try decode(Index.self, json)
}
}
@Test func rejectsManifestWithTraversingConfig() throws {
let json = """
{"schemaVersion":2,
"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"\(Self.traversal)","size":2},
"layers":[]}
"""
#expect(throws: DecodingError.self) {
try decode(Manifest.self, json)
}
}
@Test func rejectsManifestWithTraversingLayer() throws {
let json = """
{"schemaVersion":2,
"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"\(Self.valid)","size":2},
"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"\(Self.traversal)","size":3}]}
"""
#expect(throws: DecodingError.self) {
try decode(Manifest.self, json)
}
}
@Test func roundTripsThroughEncoder() throws {
let descriptor = Descriptor(
mediaType: MediaTypes.imageManifest,
digest: Self.valid,
size: 512,
annotations: ["org.opencontainers.image.title": "metadata.json"])
let data = try JSONEncoder().encode(descriptor)
let decoded = try JSONDecoder().decode(Descriptor.self, from: data)
#expect(decoded == descriptor)
}
}