fix(webdav): reject PROPPATCH on protected DAV:/oc:/nc:/ocs: props
DeadPropertyStore let PROPPATCH set any namespace/name verbatim, incl. names the server itself emits as live state (DAV: entirely, plus oc:/nc:/ocs: names used by write_file_response / write_folder_response). That either forges a live prop or stores dead rows nothing ever reads. is_protected_property() denylists them; both PROPPATCH handlers (native + NC) now return per-property 403 instead of storing. oc:favorite stays writable via its existing special-case, which runs before the protection check.
This commit is contained in:
@@ -72,6 +72,46 @@ impl std::fmt::Display for QualifiedName {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether PROPPATCH must refuse to set/remove this property as a dead
|
||||||
|
/// property (RFC 4918 §9.2 — server MAY reject a PROPPATCH attempt on a
|
||||||
|
/// live property; DeadPropertyStore has no business holding a value that
|
||||||
|
/// PROPFIND / REPORT already emit from live server state).
|
||||||
|
pub fn is_protected_property(qn: &QualifiedName) -> bool {
|
||||||
|
match qn.namespace.as_str() {
|
||||||
|
// RFC 4918 §15 — the DAV: namespace is server-owned in its
|
||||||
|
// entirety. Any PROPPATCH into it either forges a live
|
||||||
|
// property (dual-emission) or accumulates unread garbage
|
||||||
|
// (silent litter).
|
||||||
|
"DAV:" => true,
|
||||||
|
|
||||||
|
// Every name below appears verbatim in write_folder_response
|
||||||
|
// / write_file_response in the NC handler. Adding a new
|
||||||
|
// live emitter → add its name here.
|
||||||
|
"http://owncloud.org/ns" => matches!(
|
||||||
|
qn.name.as_str(),
|
||||||
|
"favorite"
|
||||||
|
| "fileid"
|
||||||
|
| "id"
|
||||||
|
| "owner-id"
|
||||||
|
| "owner-display-name"
|
||||||
|
| "permissions"
|
||||||
|
| "share-types"
|
||||||
|
| "size"
|
||||||
|
),
|
||||||
|
|
||||||
|
"http://nextcloud.org/ns" => matches!(
|
||||||
|
qn.name.as_str(),
|
||||||
|
"has-preview" | "is-encrypted" | "mount-type" | "creation_time" | "upload_time"
|
||||||
|
),
|
||||||
|
|
||||||
|
"http://open-collaboration-services.org/ns" => {
|
||||||
|
matches!(qn.name.as_str(), "share-permissions")
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// PROPFIND request type
|
/// PROPFIND request type
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum PropFindType {
|
pub enum PropFindType {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use quick_xml::Writer;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::application::adapters::webdav_adapter::{
|
use crate::application::adapters::webdav_adapter::{
|
||||||
LockInfo, PropFindRequest, PropPatchOp, QualifiedName, WebDavAdapter,
|
LockInfo, PropFindRequest, PropPatchOp, QualifiedName, WebDavAdapter, is_protected_property,
|
||||||
};
|
};
|
||||||
use crate::application::dtos::file_dto::FileDto;
|
use crate::application::dtos::file_dto::FileDto;
|
||||||
use crate::application::dtos::folder_dto::FolderDto;
|
use crate::application::dtos::folder_dto::FolderDto;
|
||||||
@@ -807,6 +807,12 @@ async fn handle_proppatch(
|
|||||||
let mut results: Vec<(&QualifiedName, bool)> = Vec::new();
|
let mut results: Vec<(&QualifiedName, bool)> = Vec::new();
|
||||||
for op in &ops {
|
for op in &ops {
|
||||||
match op {
|
match op {
|
||||||
|
PropPatchOp::Set(pv) if is_protected_property(&pv.name) => {
|
||||||
|
results.push((&pv.name, false));
|
||||||
|
}
|
||||||
|
PropPatchOp::Remove(name) if is_protected_property(name) => {
|
||||||
|
results.push((name, false));
|
||||||
|
}
|
||||||
PropPatchOp::Set(pv) => {
|
PropPatchOp::Set(pv) => {
|
||||||
dead_props
|
dead_props
|
||||||
.set(resource_ref, pv.name.clone(), pv.value.clone())
|
.set(resource_ref, pv.name.clone(), pv.value.clone())
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use std::sync::Arc;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::application::adapters::webdav_adapter::{
|
use crate::application::adapters::webdav_adapter::{
|
||||||
PropFindRequest, PropPatchOp, QualifiedName, WebDavAdapter,
|
PropFindRequest, PropPatchOp, QualifiedName, WebDavAdapter, is_protected_property,
|
||||||
};
|
};
|
||||||
use crate::application::dtos::pagination::PaginationRequestDto;
|
use crate::application::dtos::pagination::PaginationRequestDto;
|
||||||
use crate::application::ports::favorites_ports::FavoritesUseCase;
|
use crate::application::ports::favorites_ports::FavoritesUseCase;
|
||||||
@@ -556,6 +556,12 @@ async fn handle_proppatch(
|
|||||||
}
|
}
|
||||||
results.push((name, true));
|
results.push((name, true));
|
||||||
}
|
}
|
||||||
|
PropPatchOp::Set(pv) if is_protected_property(&pv.name) => {
|
||||||
|
results.push((&pv.name, false));
|
||||||
|
}
|
||||||
|
PropPatchOp::Remove(name) if is_protected_property(name) => {
|
||||||
|
results.push((name, false));
|
||||||
|
}
|
||||||
PropPatchOp::Set(pv) => {
|
PropPatchOp::Set(pv) => {
|
||||||
dead_props
|
dead_props
|
||||||
.set(resource_ref, pv.name.clone(), pv.value.clone())
|
.set(resource_ref, pv.name.clone(), pv.value.clone())
|
||||||
|
|||||||
Reference in New Issue
Block a user