From 828691e3c455b42b6ab0aae2d9c3570639c3a9e6 Mon Sep 17 00:00:00 2001 From: Arkadiusz Juszczyk Date: Wed, 24 Jun 2026 01:02:28 +0200 Subject: [PATCH] sharing: URL-encode-aware backmap of REPORT/PROPPATCH hrefs make_href percent-encodes hrefs (an '@' in an email principal becomes %40), but the backmap compared against the raw PathMapped, so the rewrite was skipped and the owner's real path leaked -- editing a shared collection then failed with 403. Compare and rewrite on the quoted form. --- CHANGELOG.md | 1 + radicale/app/proppatch.py | 5 +++-- radicale/app/report.py | 10 ++++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8efac321..b216972a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 3.7.6.dev * Extension: item verification on commandline * Improvement: catch lack of support of PERIOD in vobject <= 0.9.9 +* Fix: sharing: backmap of REPORT/PROPPATCH hrefs is now URL-encode-aware (edit of a shared collection failed when the principal contains '@') ## 3.7.5 * Add: [sharing] conversion_bday_summary_template (customize summary) diff --git a/radicale/app/proppatch.py b/radicale/app/proppatch.py index 90145329..1fc5b6de 100644 --- a/radicale/app/proppatch.py +++ b/radicale/app/proppatch.py @@ -25,6 +25,7 @@ import socket import xml.etree.ElementTree as ET from http import client from typing import Dict, Optional, Union, cast +from urllib.parse import quote import defusedxml.ElementTree as DefusedET @@ -49,8 +50,8 @@ def xml_proppatch(base_prefix: str, path: str, href = ET.Element(xmlutils.make_clark("D:href")) href.text = xmlutils.make_href(base_prefix, path) if share: - # backmap - href.text = href.text.replace(share['PathMapped'], share['PathOrToken']) + # backmap; quote so the encoded href and raw share path compare + href.text = href.text.replace(quote(share['PathMapped']), quote(str(share['PathOrToken']))) response.append(href) # Create D:propstat element for props with status 200 OK propstat = ET.Element(xmlutils.make_clark("D:propstat")) diff --git a/radicale/app/report.py b/radicale/app/report.py index 807a923a..b1f4c3dc 100644 --- a/radicale/app/report.py +++ b/radicale/app/report.py @@ -31,7 +31,7 @@ import xml.etree.ElementTree as ET from http import client from typing import (Callable, Iterable, Iterator, List, Optional, Sequence, Tuple, Union) -from urllib.parse import unquote, urlparse +from urllib.parse import quote, unquote, urlparse import vobject import vobject.base @@ -741,9 +741,11 @@ def xml_item_response(base_prefix: str, href: str, href_element.text = xmlutils.make_href(base_prefix, href) logger.trace("REPORT/xml_report: href=%r base_prefix=%r", href_element.text, base_prefix) if share: - # backmap - if href_element.text.startswith(base_prefix + share['PathMapped']): - href_element.text = base_prefix + str(share['PathOrToken']) + href_element.text.removeprefix(base_prefix + share['PathMapped']) + # backmap; quote so the encoded href and raw share path compare + mapped = quote(share['PathMapped']) + token = quote(str(share['PathOrToken'])) + if href_element.text.startswith(base_prefix + mapped): + href_element.text = base_prefix + token + href_element.text.removeprefix(base_prefix + mapped) if share_bday_automap and href_element.text.endswith(".vcf"): href_element.text = href_element.text.removesuffix(".vcf") + ".ics" logger.trace("REPORT/xml_report: href=%r (backmapped)", href_element.text)