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.
This commit is contained in:
Arkadiusz Juszczyk
2026-06-24 01:02:28 +02:00
parent 5c69056c46
commit 828691e3c4
3 changed files with 10 additions and 6 deletions

View File

@@ -3,6 +3,7 @@
## 3.7.6.dev ## 3.7.6.dev
* Extension: item verification on commandline * Extension: item verification on commandline
* Improvement: catch lack of support of PERIOD in vobject <= 0.9.9 * 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 ## 3.7.5
* Add: [sharing] conversion_bday_summary_template (customize summary) * Add: [sharing] conversion_bday_summary_template (customize summary)

View File

@@ -25,6 +25,7 @@ import socket
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from http import client from http import client
from typing import Dict, Optional, Union, cast from typing import Dict, Optional, Union, cast
from urllib.parse import quote
import defusedxml.ElementTree as DefusedET 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 = ET.Element(xmlutils.make_clark("D:href"))
href.text = xmlutils.make_href(base_prefix, path) href.text = xmlutils.make_href(base_prefix, path)
if share: if share:
# backmap # backmap; quote so the encoded href and raw share path compare
href.text = href.text.replace(share['PathMapped'], share['PathOrToken']) href.text = href.text.replace(quote(share['PathMapped']), quote(str(share['PathOrToken'])))
response.append(href) response.append(href)
# Create D:propstat element for props with status 200 OK # Create D:propstat element for props with status 200 OK
propstat = ET.Element(xmlutils.make_clark("D:propstat")) propstat = ET.Element(xmlutils.make_clark("D:propstat"))

View File

@@ -31,7 +31,7 @@ import xml.etree.ElementTree as ET
from http import client from http import client
from typing import (Callable, Iterable, Iterator, List, Optional, Sequence, from typing import (Callable, Iterable, Iterator, List, Optional, Sequence,
Tuple, Union) Tuple, Union)
from urllib.parse import unquote, urlparse from urllib.parse import quote, unquote, urlparse
import vobject import vobject
import vobject.base 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) href_element.text = xmlutils.make_href(base_prefix, href)
logger.trace("REPORT/xml_report: href=%r base_prefix=%r", href_element.text, base_prefix) logger.trace("REPORT/xml_report: href=%r base_prefix=%r", href_element.text, base_prefix)
if share: if share:
# backmap # backmap; quote so the encoded href and raw share path compare
if href_element.text.startswith(base_prefix + share['PathMapped']): mapped = quote(share['PathMapped'])
href_element.text = base_prefix + str(share['PathOrToken']) + href_element.text.removeprefix(base_prefix + 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"): if share_bday_automap and href_element.text.endswith(".vcf"):
href_element.text = href_element.text.removesuffix(".vcf") + ".ics" href_element.text = href_element.text.removesuffix(".vcf") + ".ics"
logger.trace("REPORT/xml_report: href=%r (backmapped)", href_element.text) logger.trace("REPORT/xml_report: href=%r (backmapped)", href_element.text)