Fix MOVE failing with URL-encoded Destination header

Destination header wasn't being decoded before urlparse(), and relative
URLs (empty netloc) failed the host comparison.

Ran into this with email usernames where @ gets encoded as %40."
This commit is contained in:
laurisvr
2026-01-21 16:58:25 +01:00
committed by Lauris
parent 4fb16727f0
commit 9a2bbb589c

View File

@@ -22,7 +22,7 @@ import errno
import posixpath import posixpath
import re import re
from http import client from http import client
from urllib.parse import urlparse from urllib.parse import unquote, urlparse
from radicale import httputils, pathutils, storage, types from radicale import httputils, pathutils, storage, types
from radicale.app.base import Access, ApplicationBase from radicale.app.base import Access, ApplicationBase
@@ -51,15 +51,22 @@ class ApplicationPartMove(ApplicationBase):
path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse: path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
"""Manage MOVE request.""" """Manage MOVE request."""
raw_dest = environ.get("HTTP_DESTINATION", "") raw_dest = environ.get("HTTP_DESTINATION", "")
to_url = urlparse(raw_dest)
to_netloc_with_port = to_url.netloc # Decode URL-encoded characters (e.g. %40 -> @) before parsing
if to_url.port is None: raw_dest_decoded = unquote(raw_dest)
to_netloc_with_port += (":443" if to_url.scheme == "https" to_url = urlparse(raw_dest_decoded)
else ":80")
if to_netloc_with_port != get_server_netloc(environ, force_port=True): # Only check netloc for absolute URLs
logger.info("Unsupported destination address: %r", raw_dest) if to_url.netloc:
# Remote destination server, not supported to_netloc_with_port = to_url.netloc
return httputils.REMOTE_DESTINATION if to_url.port is None:
to_netloc_with_port += (":443" if to_url.scheme == "https"
else ":80")
if to_netloc_with_port != get_server_netloc(environ, force_port=True):
logger.info("Unsupported destination address: %r", raw_dest)
# Remote destination server, not supported
return httputils.REMOTE_DESTINATION
access = Access(self._rights, user, path) access = Access(self._rights, user, path)
if not access.check("w"): if not access.check("w"):
return httputils.NOT_ALLOWED return httputils.NOT_ALLOWED