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
+13 -5
View File
@@ -540,6 +540,8 @@ pub trait ObjectProvider {
/// Fetches the canonical object projection currently published at `path`.
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)
.ok_or_else(|| CallError::new(CallErrorKind::NotFound, "target path is not published"))
.and_then(|object| {
@@ -552,12 +554,15 @@ pub trait ObjectProvider {
/// Resolves and executes a canonical path-targeted operation request.
///
/// Failure checks follow the exact canonical precedence: path, supplied
/// object validator mismatch, membership, interface validator requirement
/// and mismatch, operation existence, object validator requirement,
/// arguments, then authorization. No failed precondition invokes the typed
/// implementation.
/// Structural path validation precedes provider resolution. For a valid
/// request, failure checks follow the exact canonical precedence: path,
/// supplied object validator mismatch, membership, interface validator
/// requirement and mismatch, operation existence, object validator
/// requirement, arguments, then authorization. No failed precondition
/// invokes the provider callback or typed implementation.
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();
self.with_object(&path, |publication| {
publication.dispatch_resolved(request, |object, interface, operation| {
@@ -592,6 +597,8 @@ impl Error for PublicationError {}
/// Stable host dispatch failure kind.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CallErrorKind {
/// The request is structurally invalid.
InvalidRequest,
/// The target path is not currently published.
NotFound,
/// The selected interface is not exposed by the current object.
@@ -671,6 +678,7 @@ impl CallError {
#[must_use]
pub fn protocol_error(&self) -> ProtocolError {
let code = match self.kind {
CallErrorKind::InvalidRequest => ProtocolErrorCode::InvalidRequest,
CallErrorKind::NotFound => ProtocolErrorCode::NotFound,
CallErrorKind::InterfaceMismatch => ProtocolErrorCode::InterfaceMismatch,
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() {
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(
provider
.call_operation(request(