Validate container ID from XPC requests (#1956)

Signed-off-by: Kathryn Baldauf <k_baldauf@apple.com>
This commit is contained in:
Kathryn Baldauf
2026-07-20 10:42:37 -07:00
committed by GitHub
parent 14233cee65
commit 9e1d6e8b6e
8 changed files with 44 additions and 14 deletions
@@ -200,7 +200,9 @@ extension Application {
useRosetta ? nil : "--enable-qemu",
].compactMap { $0 }
try ContainerAPIClient.Utility.validEntityName(Builder.builderContainerId)
guard ManagedContainer.nameValid(Builder.builderContainerId) else {
throw ContainerizationError(.invalidArgument, message: "container ID \(Builder.builderContainerId) is not a valid container ID")
}
let image = try await ClientImage.fetch(
reference: builderImage,
@@ -70,7 +70,10 @@ extension Application {
progress.start()
let id = Utility.createContainerID(name: self.managementFlags.name)
try Utility.validEntityName(id)
guard ManagedContainer.nameValid(id) else {
throw ContainerizationError(.invalidArgument, message: "container ID \(id) is not a valid container ID")
}
let ck = try await Utility.containerConfigFromFlags(
id: id,
@@ -80,7 +80,9 @@ extension Application {
}
progress.start()
try Utility.validEntityName(id)
guard ManagedContainer.nameValid(id) else {
throw ContainerizationError(.invalidArgument, message: "container ID \(id) is not a valid container ID")
}
// Check if container with id already exists.
let client = ContainerClient()
@@ -17,6 +17,7 @@
import ArgumentParser
import ContainerAPIClient
import ContainerPersistence
import ContainerResource
import ContainerizationError
import ContainerizationOCI
import Foundation
@@ -115,7 +116,9 @@ extension Application {
id = "\(imageName)-\(suffix)"
}
try Utility.validEntityName(id)
guard ManagedContainer.nameValid(id) else {
throw ContainerizationError(.invalidArgument, message: "machine ID \(id) is not a valid machine ID")
}
let client = MachineClient()
let (config, resources) = try await MachineClient.machineConfigFromFlags(
@@ -41,9 +41,12 @@ public struct ManagedContainer: ManagedResource {
/// not the protocol's 64-hex default.
public static func generateId() -> String { UUID().uuidString.lowercased() }
/// Container name rule (mixed case, dots, hyphens, underscores). Duplicated from
/// Utility.validEntityName
/// Container name rule
public static func nameValid(_ name: String) -> Bool {
// Maximum Linux hostname length is 64, but limit to maximum DNS label length
guard name.count <= 63 else {
return false
}
let pattern = #"^[a-zA-Z0-9][a-zA-Z0-9_.-]+$"#
return name.range(of: pattern, options: .regularExpression) != nil
}
@@ -56,14 +56,6 @@ public struct Utility {
return String(hex.prefix(12))
}
public static func validEntityName(_ name: String) throws {
let pattern = #"^[a-zA-Z0-9][a-zA-Z0-9_.-]+$"#
let regex = try Regex(pattern)
if try regex.firstMatch(in: name) == nil {
throw ContainerizationError(.invalidArgument, message: "invalid entity name \(name)")
}
}
public static func validMACAddress(_ macAddress: String) throws {
let pattern = #"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"#
let regex = try Regex(pattern)
@@ -54,6 +54,9 @@ public struct ContainersHarness: Sendable {
message: "id cannot be empty"
)
}
guard ManagedContainer.nameValid(id) else {
throw ContainerizationError(.invalidArgument, message: "container ID \(id) is not a valid container ID")
}
let stdio = message.stdio()
let data = message.dataNoCopy(key: .dynamicEnv)
@@ -193,6 +196,9 @@ public struct ContainersHarness: Sendable {
options = try JSONDecoder().decode(ContainerCreateOptions.self, from: odata)
}
let config = try JSONDecoder().decode(ContainerConfiguration.self, from: data)
guard ManagedContainer.nameValid(config.id) else {
throw ContainerizationError(.invalidArgument, message: "container ID \(config.id) is not a valid container ID")
}
let kernel = try JSONDecoder().decode(Kernel.self, from: kdata)
let initImage = message.string(key: .initImage)
@@ -262,6 +268,9 @@ public struct ContainersHarness: Sendable {
guard let id else {
throw ContainerizationError(.invalidArgument, message: "id cannot be empty")
}
guard ManagedContainer.nameValid(id) else {
throw ContainerizationError(.invalidArgument, message: "container ID \(id) is not a valid container ID")
}
let forceDelete = message.bool(key: .forceDelete)
try await service.delete(id: id, force: forceDelete)
return message.reply()
@@ -272,6 +281,9 @@ public struct ContainersHarness: Sendable {
guard let containerId = message.string(key: .id) else {
throw ContainerizationError(.invalidArgument, message: "id cannot be empty")
}
guard ManagedContainer.nameValid(containerId) else {
throw ContainerizationError(.invalidArgument, message: "container ID \(containerId) is not a valid container ID")
}
let size = try await service.containerDiskUsage(id: containerId)
@@ -289,6 +301,9 @@ public struct ContainersHarness: Sendable {
message: "id cannot be empty"
)
}
guard ManagedContainer.nameValid(id) else {
throw ContainerizationError(.invalidArgument, message: "container ID \(id) is not a valid container ID")
}
let fds = try await service.logs(id: id)
let reply = message.reply()
try reply.set(key: .logs, value: fds)
@@ -374,6 +389,9 @@ public struct ContainersHarness: Sendable {
message: "id cannot be empty"
)
}
guard ManagedContainer.nameValid(id) else {
throw ContainerizationError(.invalidArgument, message: "container ID \(id) is not a valid container ID")
}
let archive = message.string(key: .archive)
guard let archive else {
throw ContainerizationError(
@@ -60,6 +60,13 @@ struct ManagedContainerTests {
#expect(!ManagedContainer.nameValid("a b"))
}
@Test func nameValidRejectsNamesLongerThan63Characters() {
let maxValidName = String(repeating: "a", count: 63)
let tooLongName = String(repeating: "a", count: 64)
#expect(ManagedContainer.nameValid(maxValidName))
#expect(!ManagedContainer.nameValid(tooLongName))
}
@Test func generateIdIsLowercasedUUID() {
let id = ManagedContainer.generateId()
#expect(id == id.lowercased())