fix: reject malformed host target paths

This commit is contained in:
2026-09-29 06:35:10 +09:00
parent 2efda433e7
commit 653a1171a2
3 changed files with 47 additions and 7 deletions
+3 -2
View File
@@ -175,8 +175,9 @@ descriptor; generated descriptors contain neither those references nor Rust
trait identity. trait identity.
Dispatch is qualified by interface reference and operation name and targets a Dispatch is qualified by interface reference and operation name and targets a
canonical Worldspace path. An `ObjectProvider` resolves that path exactly once canonical Worldspace path. An `ObjectProvider` validates that path before
inside `with_object`; the host then checks the supplied object validator, resolution, maps malformed paths to `InvalidRequest`, then resolves a valid path
exactly once inside `with_object`; the host checks the supplied object validator,
current interface membership, interface validator, descriptor operation, current interface membership, interface validator, descriptor operation,
operation-specific object-validator policy, arguments, and authorization in operation-specific object-validator policy, arguments, and authorization in
canonical precedence. Path and children remain provider resolution context and canonical precedence. Path and children remain provider resolution context and
+13 -5
View File
@@ -540,6 +540,8 @@ pub trait ObjectProvider {
/// Fetches the canonical object projection currently published at `path`. /// Fetches the canonical object projection currently published at `path`.
fn fetch(&self, path: &str) -> Result<Object, CallError> { fn fetch(&self, path: &str) -> Result<Object, CallError> {
wip_protocol::validate_path(path)
.map_err(|error| CallError::new(CallErrorKind::InvalidRequest, error.to_string()))?;
self.with_object(path, PublishedObject::protocol_object) self.with_object(path, PublishedObject::protocol_object)
.ok_or_else(|| CallError::new(CallErrorKind::NotFound, "target path is not published")) .ok_or_else(|| CallError::new(CallErrorKind::NotFound, "target path is not published"))
.and_then(|object| { .and_then(|object| {
@@ -552,12 +554,15 @@ pub trait ObjectProvider {
/// Resolves and executes a canonical path-targeted operation request. /// Resolves and executes a canonical path-targeted operation request.
/// ///
/// Failure checks follow the exact canonical precedence: path, supplied /// Structural path validation precedes provider resolution. For a valid
/// object validator mismatch, membership, interface validator requirement /// request, failure checks follow the exact canonical precedence: path,
/// and mismatch, operation existence, object validator requirement, /// supplied object validator mismatch, membership, interface validator
/// arguments, then authorization. No failed precondition invokes the typed /// requirement and mismatch, operation existence, object validator
/// implementation. /// requirement, arguments, then authorization. No failed precondition
/// invokes the provider callback or typed implementation.
fn call_operation(&self, request: CallRequest) -> Result<CallResult, CallError> { fn call_operation(&self, request: CallRequest) -> Result<CallResult, CallError> {
wip_protocol::validate_path(&request.target.path)
.map_err(|error| CallError::new(CallErrorKind::InvalidRequest, error.to_string()))?;
let path = request.target.path.clone(); let path = request.target.path.clone();
self.with_object(&path, |publication| { self.with_object(&path, |publication| {
publication.dispatch_resolved(request, |object, interface, operation| { publication.dispatch_resolved(request, |object, interface, operation| {
@@ -592,6 +597,8 @@ impl Error for PublicationError {}
/// Stable host dispatch failure kind. /// Stable host dispatch failure kind.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CallErrorKind { pub enum CallErrorKind {
/// The request is structurally invalid.
InvalidRequest,
/// The target path is not currently published. /// The target path is not currently published.
NotFound, NotFound,
/// The selected interface is not exposed by the current object. /// The selected interface is not exposed by the current object.
@@ -671,6 +678,7 @@ impl CallError {
#[must_use] #[must_use]
pub fn protocol_error(&self) -> ProtocolError { pub fn protocol_error(&self) -> ProtocolError {
let code = match self.kind { let code = match self.kind {
CallErrorKind::InvalidRequest => ProtocolErrorCode::InvalidRequest,
CallErrorKind::NotFound => ProtocolErrorCode::NotFound, CallErrorKind::NotFound => ProtocolErrorCode::NotFound,
CallErrorKind::InterfaceMismatch => ProtocolErrorCode::InterfaceMismatch, CallErrorKind::InterfaceMismatch => ProtocolErrorCode::InterfaceMismatch,
CallErrorKind::OperationNotFound => ProtocolErrorCode::OperationNotFound, CallErrorKind::OperationNotFound => ProtocolErrorCode::OperationNotFound,
+31
View File
@@ -440,6 +440,37 @@ fn path_dispatch_is_interface_qualified_and_resolves_once_per_call() {
fn call_failures_follow_canonical_precedence_without_execution() { fn call_failures_follow_canonical_precedence_without_execution() {
let (_, provider, hidden) = registry_and_provider(); let (_, provider, hidden) = registry_and_provider();
let resolutions = provider.resolutions.get();
for invalid_path in [
"relative",
"/trailing/",
"/double//segment",
"/dot/./segment",
"/parent/../segment",
] {
assert_failure(
provider
.call_operation(request(
invalid_path,
Some(b"stale"),
hidden.reference(),
Some(b"stale"),
"missing",
BTreeMap::new(),
))
.unwrap_err(),
CallErrorKind::InvalidRequest,
ProtocolErrorCode::InvalidRequest,
);
}
assert_eq!(provider.resolutions.get(), resolutions);
assert_failure(
provider.fetch("/invalid/").unwrap_err(),
CallErrorKind::InvalidRequest,
ProtocolErrorCode::InvalidRequest,
);
assert_eq!(provider.resolutions.get(), resolutions);
assert_failure( assert_failure(
provider provider
.call_operation(request( .call_operation(request(