assert sanitized and stripped paths
This commit is contained in:
parent
c08754cf92
commit
5429f5c1a9
@ -324,8 +324,8 @@ class Application(
|
|||||||
if permissions and self.Rights.authorized(user, path, permissions):
|
if permissions and self.Rights.authorized(user, path, permissions):
|
||||||
return True
|
return True
|
||||||
if parent_permissions:
|
if parent_permissions:
|
||||||
parent_path = pathutils.sanitize_path(
|
parent_path = pathutils.unstrip_path(
|
||||||
"/%s/" % posixpath.dirname(path.strip("/")))
|
posixpath.dirname(pathutils.strip_path(path)), True)
|
||||||
if self.Rights.authorized(user, parent_path, parent_permissions):
|
if self.Rights.authorized(user, parent_path, parent_permissions):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
@ -28,7 +28,7 @@ import posixpath
|
|||||||
from http import client
|
from http import client
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
from radicale import httputils, storage, xmlutils
|
from radicale import httputils, pathutils, storage, xmlutils
|
||||||
from radicale.log import logger
|
from radicale.log import logger
|
||||||
|
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ class ApplicationGetMixin:
|
|||||||
def do_GET(self, environ, base_prefix, path, user):
|
def do_GET(self, environ, base_prefix, path, user):
|
||||||
"""Manage GET request."""
|
"""Manage GET request."""
|
||||||
# Redirect to .web if the root URL is requested
|
# Redirect to .web if the root URL is requested
|
||||||
if not path.strip("/"):
|
if not pathutils.strip_path(path):
|
||||||
web_path = ".web"
|
web_path = ".web"
|
||||||
if not environ.get("PATH_INFO"):
|
if not environ.get("PATH_INFO"):
|
||||||
web_path = posixpath.join(posixpath.basename(base_prefix),
|
web_path = posixpath.join(posixpath.basename(base_prefix),
|
||||||
|
@ -63,8 +63,8 @@ class ApplicationMkcalendarMixin:
|
|||||||
if item:
|
if item:
|
||||||
return self.webdav_error_response(
|
return self.webdav_error_response(
|
||||||
"D", "resource-must-be-null")
|
"D", "resource-must-be-null")
|
||||||
parent_path = pathutils.sanitize_path(
|
parent_path = pathutils.unstrip_path(
|
||||||
"/%s/" % posixpath.dirname(path.strip("/")))
|
posixpath.dirname(pathutils.strip_path(path)), True)
|
||||||
parent_item = next(self.Collection.discover(parent_path), None)
|
parent_item = next(self.Collection.discover(parent_path), None)
|
||||||
if not parent_item:
|
if not parent_item:
|
||||||
return httputils.CONFLICT
|
return httputils.CONFLICT
|
||||||
|
@ -64,8 +64,8 @@ class ApplicationMkcolMixin:
|
|||||||
item = next(self.Collection.discover(path), None)
|
item = next(self.Collection.discover(path), None)
|
||||||
if item:
|
if item:
|
||||||
return httputils.METHOD_NOT_ALLOWED
|
return httputils.METHOD_NOT_ALLOWED
|
||||||
parent_path = pathutils.sanitize_path(
|
parent_path = pathutils.unstrip_path(
|
||||||
"/%s/" % posixpath.dirname(path.strip("/")))
|
posixpath.dirname(pathutils.strip_path(path)), True)
|
||||||
parent_item = next(self.Collection.discover(parent_path), None)
|
parent_item = next(self.Collection.discover(parent_path), None)
|
||||||
if not parent_item:
|
if not parent_item:
|
||||||
return httputils.CONFLICT
|
return httputils.CONFLICT
|
||||||
|
@ -66,8 +66,8 @@ class ApplicationMoveMixin:
|
|||||||
to_item = next(self.Collection.discover(to_path), None)
|
to_item = next(self.Collection.discover(to_path), None)
|
||||||
if isinstance(to_item, storage.BaseCollection):
|
if isinstance(to_item, storage.BaseCollection):
|
||||||
return httputils.FORBIDDEN
|
return httputils.FORBIDDEN
|
||||||
to_parent_path = pathutils.sanitize_path(
|
to_parent_path = pathutils.unstrip_path(
|
||||||
"/%s/" % posixpath.dirname(to_path.strip("/")))
|
posixpath.dirname(pathutils.strip_path(to_path)), True)
|
||||||
to_collection = next(
|
to_collection = next(
|
||||||
self.Collection.discover(to_parent_path), None)
|
self.Collection.discover(to_parent_path), None)
|
||||||
if not to_collection:
|
if not to_collection:
|
||||||
@ -83,7 +83,7 @@ class ApplicationMoveMixin:
|
|||||||
to_collection.has_uid(item.uid)):
|
to_collection.has_uid(item.uid)):
|
||||||
return self.webdav_error_response(
|
return self.webdav_error_response(
|
||||||
"C" if tag == "VCALENDAR" else "CR", "no-uid-conflict")
|
"C" if tag == "VCALENDAR" else "CR", "no-uid-conflict")
|
||||||
to_href = posixpath.basename(to_path.strip("/"))
|
to_href = posixpath.basename(pathutils.strip_path(to_path))
|
||||||
try:
|
try:
|
||||||
self.Collection.move(item, to_collection, to_href)
|
self.Collection.move(item, to_collection, to_href)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
@ -95,9 +95,10 @@ def xml_propfind_response(base_prefix, path, item, props, user, write=False,
|
|||||||
href = ET.Element(xmlutils.make_tag("D", "href"))
|
href = ET.Element(xmlutils.make_tag("D", "href"))
|
||||||
if is_collection:
|
if is_collection:
|
||||||
# Some clients expect collections to end with /
|
# Some clients expect collections to end with /
|
||||||
uri = "/%s/" % item.path if item.path else "/"
|
uri = pathutils.unstrip_path(item.path, True)
|
||||||
else:
|
else:
|
||||||
uri = "/" + posixpath.join(collection.path, item.href)
|
uri = pathutils.unstrip_path(
|
||||||
|
posixpath.join(collection.path, item.href))
|
||||||
|
|
||||||
href.text = xmlutils.make_href(base_prefix, uri)
|
href.text = xmlutils.make_href(base_prefix, uri)
|
||||||
response.append(href)
|
response.append(href)
|
||||||
@ -335,7 +336,7 @@ class ApplicationPropfindMixin:
|
|||||||
"""Get items from request that user is allowed to access."""
|
"""Get items from request that user is allowed to access."""
|
||||||
for item in items:
|
for item in items:
|
||||||
if isinstance(item, storage.BaseCollection):
|
if isinstance(item, storage.BaseCollection):
|
||||||
path = pathutils.sanitize_path("/%s/" % item.path)
|
path = pathutils.unstrip_path(item.path, True)
|
||||||
if item.get_meta("tag"):
|
if item.get_meta("tag"):
|
||||||
permissions = self.Rights.authorized(user, path, "rw")
|
permissions = self.Rights.authorized(user, path, "rw")
|
||||||
target = "collection with tag %r" % item.path
|
target = "collection with tag %r" % item.path
|
||||||
@ -343,7 +344,7 @@ class ApplicationPropfindMixin:
|
|||||||
permissions = self.Rights.authorized(user, path, "RW")
|
permissions = self.Rights.authorized(user, path, "RW")
|
||||||
target = "collection %r" % item.path
|
target = "collection %r" % item.path
|
||||||
else:
|
else:
|
||||||
path = pathutils.sanitize_path("/%s/" % item.collection.path)
|
path = pathutils.unstrip_path(item.collection.path, True)
|
||||||
permissions = self.Rights.authorized(user, path, "rw")
|
permissions = self.Rights.authorized(user, path, "rw")
|
||||||
target = "item %r from %r" % (item.href, item.collection.path)
|
target = "item %r from %r" % (item.href, item.collection.path)
|
||||||
if rights.intersect_permissions(permissions, "Ww"):
|
if rights.intersect_permissions(permissions, "Ww"):
|
||||||
|
@ -52,8 +52,8 @@ class ApplicationPutMixin:
|
|||||||
logger.debug("client timed out", exc_info=True)
|
logger.debug("client timed out", exc_info=True)
|
||||||
return httputils.REQUEST_TIMEOUT
|
return httputils.REQUEST_TIMEOUT
|
||||||
# Prepare before locking
|
# Prepare before locking
|
||||||
parent_path = pathutils.sanitize_path(
|
parent_path = pathutils.unstrip_path(
|
||||||
"/%s/" % posixpath.dirname(path.strip("/")))
|
posixpath.dirname(pathutils.strip_path(path)), True)
|
||||||
permissions = self.Rights.authorized(user, path, "Ww")
|
permissions = self.Rights.authorized(user, path, "Ww")
|
||||||
parent_permissions = self.Rights.authorized(user, parent_path, "w")
|
parent_permissions = self.Rights.authorized(user, parent_path, "w")
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ class ApplicationPutMixin:
|
|||||||
vobject_items, tags.get(content_type))
|
vobject_items, tags.get(content_type))
|
||||||
if not tag:
|
if not tag:
|
||||||
raise ValueError("Can't determine collection tag")
|
raise ValueError("Can't determine collection tag")
|
||||||
collection_path = pathutils.sanitize_path(path).strip("/")
|
collection_path = pathutils.strip_path(path)
|
||||||
elif (write_whole_collection is not None and
|
elif (write_whole_collection is not None and
|
||||||
not write_whole_collection or
|
not write_whole_collection or
|
||||||
not permissions and parent_permissions):
|
not permissions and parent_permissions):
|
||||||
@ -78,7 +78,7 @@ class ApplicationPutMixin:
|
|||||||
tag = storage.predict_tag_of_parent_collection(
|
tag = storage.predict_tag_of_parent_collection(
|
||||||
vobject_items)
|
vobject_items)
|
||||||
collection_path = posixpath.dirname(
|
collection_path = posixpath.dirname(
|
||||||
pathutils.sanitize_path(path).strip("/"))
|
pathutils.strip_path(path))
|
||||||
props = None
|
props = None
|
||||||
stored_exc_info = None
|
stored_exc_info = None
|
||||||
items = []
|
items = []
|
||||||
@ -218,7 +218,7 @@ class ApplicationPutMixin:
|
|||||||
"C" if tag == "VCALENDAR" else "CR",
|
"C" if tag == "VCALENDAR" else "CR",
|
||||||
"no-uid-conflict")
|
"no-uid-conflict")
|
||||||
|
|
||||||
href = posixpath.basename(path.strip("/"))
|
href = posixpath.basename(pathutils.strip_path(path))
|
||||||
try:
|
try:
|
||||||
etag = parent_item.upload(href, prepared_item).etag
|
etag = parent_item.upload(href, prepared_item).etag
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
@ -100,7 +100,8 @@ def xml_report(base_prefix, path, xml_request, collection, unlock_storage_fn):
|
|||||||
old_sync_token, e, exc_info=True)
|
old_sync_token, e, exc_info=True)
|
||||||
return (client.CONFLICT,
|
return (client.CONFLICT,
|
||||||
xmlutils.webdav_error("D", "valid-sync-token"))
|
xmlutils.webdav_error("D", "valid-sync-token"))
|
||||||
hreferences = ("/" + posixpath.join(collection.path, n) for n in names)
|
hreferences = (pathutils.unstrip_path(
|
||||||
|
posixpath.join(collection.path, n)) for n in names)
|
||||||
# Append current sync token to response
|
# Append current sync token to response
|
||||||
sync_token_element = ET.Element(xmlutils.make_tag("D", "sync-token"))
|
sync_token_element = ET.Element(xmlutils.make_tag("D", "sync-token"))
|
||||||
sync_token_element.text = sync_token
|
sync_token_element.text = sync_token
|
||||||
@ -142,7 +143,8 @@ def xml_report(base_prefix, path, xml_request, collection, unlock_storage_fn):
|
|||||||
|
|
||||||
for name, item in collection.get_multi(get_names()):
|
for name, item in collection.get_multi(get_names()):
|
||||||
if not item:
|
if not item:
|
||||||
uri = "/" + posixpath.join(collection.path, name)
|
uri = pathutils.unstrip_path(
|
||||||
|
posixpath.join(collection.path, name))
|
||||||
response = xml_item_response(base_prefix, uri,
|
response = xml_item_response(base_prefix, uri,
|
||||||
found_item=False)
|
found_item=False)
|
||||||
multistatus.append(response)
|
multistatus.append(response)
|
||||||
@ -223,7 +225,8 @@ def xml_report(base_prefix, path, xml_request, collection, unlock_storage_fn):
|
|||||||
else:
|
else:
|
||||||
not_found_props.append(element)
|
not_found_props.append(element)
|
||||||
|
|
||||||
uri = "/" + posixpath.join(collection.path, item.href)
|
uri = pathutils.unstrip_path(
|
||||||
|
posixpath.join(collection.path, item.href))
|
||||||
multistatus.append(xml_item_response(
|
multistatus.append(xml_item_response(
|
||||||
base_prefix, uri, found_props=found_props,
|
base_prefix, uri, found_props=found_props,
|
||||||
not_found_props=not_found_props, found_item=True))
|
not_found_props=not_found_props, found_item=True))
|
||||||
|
@ -25,6 +25,7 @@ from random import getrandbits
|
|||||||
|
|
||||||
import vobject
|
import vobject
|
||||||
|
|
||||||
|
from radicale import pathutils
|
||||||
from radicale.item import filter as radicale_filter
|
from radicale.item import filter as radicale_filter
|
||||||
|
|
||||||
|
|
||||||
@ -297,6 +298,8 @@ class Item:
|
|||||||
raise ValueError("at least one of 'collection_path' or "
|
raise ValueError("at least one of 'collection_path' or "
|
||||||
"'collection' must be set")
|
"'collection' must be set")
|
||||||
collection_path = collection.path
|
collection_path = collection.path
|
||||||
|
assert collection_path == pathutils.strip_path(
|
||||||
|
pathutils.sanitize_path(collection_path))
|
||||||
self._collection_path = collection_path
|
self._collection_path = collection_path
|
||||||
self.collection = collection
|
self.collection = collection
|
||||||
self.href = href
|
self.href = href
|
||||||
|
@ -125,6 +125,20 @@ def fsync(fd):
|
|||||||
os.fsync(fd)
|
os.fsync(fd)
|
||||||
|
|
||||||
|
|
||||||
|
def strip_path(path):
|
||||||
|
assert sanitize_path(path) == path
|
||||||
|
return path.strip("/")
|
||||||
|
|
||||||
|
|
||||||
|
def unstrip_path(stripped_path, trailing_slash=False):
|
||||||
|
assert strip_path(sanitize_path(stripped_path)) == stripped_path
|
||||||
|
assert stripped_path or trailing_slash
|
||||||
|
path = "/%s" % stripped_path
|
||||||
|
if trailing_slash and not path.endswith("/"):
|
||||||
|
path += "/"
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
def sanitize_path(path):
|
def sanitize_path(path):
|
||||||
"""Make path absolute with leading slash to prevent access to other data.
|
"""Make path absolute with leading slash to prevent access to other data.
|
||||||
|
|
||||||
@ -165,30 +179,31 @@ def is_safe_filesystem_path_component(path):
|
|||||||
is_safe_path_component(path))
|
is_safe_path_component(path))
|
||||||
|
|
||||||
|
|
||||||
def path_to_filesystem(root, *paths):
|
def path_to_filesystem(root, sane_path):
|
||||||
"""Convert path to a local filesystem path relative to base_folder.
|
"""Convert `sane_path` to a local filesystem path relative to `root`.
|
||||||
|
|
||||||
`root` must be a secure filesystem path, it will be prepend to the path.
|
`root` must be a secure filesystem path, it will be prepend to the path.
|
||||||
|
|
||||||
Conversion of `paths` is done in a secure manner, or raises ``ValueError``.
|
`sane_path` must be a sanitized path without leading or trailing ``/``.
|
||||||
|
|
||||||
|
Conversion of `sane_path` is done in a secure manner,
|
||||||
|
or raises ``ValueError``.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
paths = [sanitize_path(path).strip("/") for path in paths]
|
assert sane_path == strip_path(sanitize_path(sane_path))
|
||||||
safe_path = root
|
safe_path = root
|
||||||
for path in paths:
|
parts = sane_path.split("/") if sane_path else []
|
||||||
if not path:
|
for part in parts:
|
||||||
continue
|
if not is_safe_filesystem_path_component(part):
|
||||||
for part in path.split("/"):
|
raise UnsafePathError(part)
|
||||||
if not is_safe_filesystem_path_component(part):
|
safe_path_parent = safe_path
|
||||||
raise UnsafePathError(part)
|
safe_path = os.path.join(safe_path, part)
|
||||||
safe_path_parent = safe_path
|
# Check for conflicting files (e.g. case-insensitive file systems
|
||||||
safe_path = os.path.join(safe_path, part)
|
# or short names on Windows file systems)
|
||||||
# Check for conflicting files (e.g. case-insensitive file systems
|
if (os.path.lexists(safe_path) and
|
||||||
# or short names on Windows file systems)
|
part not in (e.name for e in
|
||||||
if (os.path.lexists(safe_path) and
|
os.scandir(safe_path_parent))):
|
||||||
part not in (e.name for e in
|
raise CollidingPathError(part)
|
||||||
os.scandir(safe_path_parent))):
|
|
||||||
raise CollidingPathError(part)
|
|
||||||
return safe_path
|
return safe_path
|
||||||
|
|
||||||
|
|
||||||
@ -206,11 +221,11 @@ class CollidingPathError(ValueError):
|
|||||||
|
|
||||||
def name_from_path(path, collection):
|
def name_from_path(path, collection):
|
||||||
"""Return Radicale item name from ``path``."""
|
"""Return Radicale item name from ``path``."""
|
||||||
path = path.strip("/") + "/"
|
assert sanitize_path(path) == path
|
||||||
start = collection.path + "/"
|
start = unstrip_path(collection.path, True)
|
||||||
if not path.startswith(start):
|
if not (path + "/").startswith(start):
|
||||||
raise ValueError("%r doesn't start with %r" % (path, start))
|
raise ValueError("%r doesn't start with %r" % (path, start))
|
||||||
name = path[len(start):][:-1]
|
name = path[len(start):]
|
||||||
if name and not is_safe_path_component(name):
|
if name and not is_safe_path_component(name):
|
||||||
raise ValueError("%r is not a component in collection %r" %
|
raise ValueError("%r is not a component in collection %r" %
|
||||||
(name, collection.path))
|
(name, collection.path))
|
||||||
|
@ -27,7 +27,7 @@ class Rights(rights.BaseRights):
|
|||||||
def authorized(self, user, path, permissions):
|
def authorized(self, user, path, permissions):
|
||||||
if self._verify_user and not user:
|
if self._verify_user and not user:
|
||||||
return ""
|
return ""
|
||||||
sane_path = pathutils.sanitize_path(path).strip("/")
|
sane_path = pathutils.strip_path(path)
|
||||||
if "/" not in sane_path:
|
if "/" not in sane_path:
|
||||||
return rights.intersect_permissions(permissions, "RW")
|
return rights.intersect_permissions(permissions, "RW")
|
||||||
if sane_path.count("/") == 1:
|
if sane_path.count("/") == 1:
|
||||||
|
@ -30,7 +30,7 @@ class Rights(rights.BaseRights):
|
|||||||
|
|
||||||
def authorized(self, user, path, permissions):
|
def authorized(self, user, path, permissions):
|
||||||
user = user or ""
|
user = user or ""
|
||||||
sane_path = pathutils.sanitize_path(path).strip("/")
|
sane_path = pathutils.strip_path(path)
|
||||||
# Prevent "regex injection"
|
# Prevent "regex injection"
|
||||||
user_escaped = re.escape(user)
|
user_escaped = re.escape(user)
|
||||||
sane_path_escaped = re.escape(sane_path)
|
sane_path_escaped = re.escape(sane_path)
|
||||||
|
@ -23,7 +23,7 @@ class Rights(authenticated.Rights):
|
|||||||
def authorized(self, user, path, permissions):
|
def authorized(self, user, path, permissions):
|
||||||
if self._verify_user and not user:
|
if self._verify_user and not user:
|
||||||
return ""
|
return ""
|
||||||
sane_path = pathutils.sanitize_path(path).strip("/")
|
sane_path = pathutils.strip_path(path)
|
||||||
if not sane_path:
|
if not sane_path:
|
||||||
return rights.intersect_permissions(permissions, "R")
|
return rights.intersect_permissions(permissions, "R")
|
||||||
if self._verify_user and user != sane_path.split("/", maxsplit=1)[0]:
|
if self._verify_user and user != sane_path.split("/", maxsplit=1)[0]:
|
||||||
|
@ -23,7 +23,7 @@ class Rights(authenticated.Rights):
|
|||||||
def authorized(self, user, path, permissions):
|
def authorized(self, user, path, permissions):
|
||||||
if self._verify_user and not user:
|
if self._verify_user and not user:
|
||||||
return ""
|
return ""
|
||||||
sane_path = pathutils.sanitize_path(path).strip("/")
|
sane_path = pathutils.strip_path(path)
|
||||||
if not sane_path:
|
if not sane_path:
|
||||||
return rights.intersect_permissions(permissions, "R")
|
return rights.intersect_permissions(permissions, "R")
|
||||||
if self._verify_user:
|
if self._verify_user:
|
||||||
|
@ -54,7 +54,7 @@ class Collection(storage.BaseCollection):
|
|||||||
def __init__(self, path, filesystem_path=None):
|
def __init__(self, path, filesystem_path=None):
|
||||||
folder = self._get_collection_root_folder()
|
folder = self._get_collection_root_folder()
|
||||||
# Path should already be sanitized
|
# Path should already be sanitized
|
||||||
self.path = pathutils.sanitize_path(path).strip("/")
|
self.path = pathutils.strip_path(path)
|
||||||
self._encoding = self.configuration.get("encoding", "stock")
|
self._encoding = self.configuration.get("encoding", "stock")
|
||||||
if filesystem_path is None:
|
if filesystem_path is None:
|
||||||
filesystem_path = pathutils.path_to_filesystem(folder, self.path)
|
filesystem_path = pathutils.path_to_filesystem(folder, self.path)
|
||||||
@ -142,7 +142,7 @@ class Collection(storage.BaseCollection):
|
|||||||
def discover(cls, path, depth="0", child_context_manager=(
|
def discover(cls, path, depth="0", child_context_manager=(
|
||||||
lambda path, href=None: contextlib.ExitStack())):
|
lambda path, href=None: contextlib.ExitStack())):
|
||||||
# Path should already be sanitized
|
# Path should already be sanitized
|
||||||
sane_path = pathutils.sanitize_path(path).strip("/")
|
sane_path = pathutils.strip_path(path)
|
||||||
attributes = sane_path.split("/") if sane_path else []
|
attributes = sane_path.split("/") if sane_path else []
|
||||||
|
|
||||||
folder = cls._get_collection_root_folder()
|
folder = cls._get_collection_root_folder()
|
||||||
@ -166,7 +166,7 @@ class Collection(storage.BaseCollection):
|
|||||||
href = None
|
href = None
|
||||||
|
|
||||||
sane_path = "/".join(attributes)
|
sane_path = "/".join(attributes)
|
||||||
collection = cls(sane_path)
|
collection = cls(pathutils.unstrip_path(sane_path, True))
|
||||||
|
|
||||||
if href:
|
if href:
|
||||||
yield collection.get(href)
|
yield collection.get(href)
|
||||||
@ -178,7 +178,8 @@ class Collection(storage.BaseCollection):
|
|||||||
return
|
return
|
||||||
|
|
||||||
for href in collection.list():
|
for href in collection.list():
|
||||||
with child_context_manager(sane_path, href):
|
with child_context_manager(
|
||||||
|
pathutils.unstrip_path(sane_path, True), href):
|
||||||
yield collection.get(href)
|
yield collection.get(href)
|
||||||
|
|
||||||
for entry in os.scandir(filesystem_path):
|
for entry in os.scandir(filesystem_path):
|
||||||
@ -190,7 +191,8 @@ class Collection(storage.BaseCollection):
|
|||||||
logger.debug("Skipping collection %r in %r",
|
logger.debug("Skipping collection %r in %r",
|
||||||
href, sane_path)
|
href, sane_path)
|
||||||
continue
|
continue
|
||||||
child_path = posixpath.join(sane_path, href)
|
child_path = pathutils.unstrip_path(
|
||||||
|
posixpath.join(sane_path, href), True)
|
||||||
with child_context_manager(child_path):
|
with child_context_manager(child_path):
|
||||||
yield cls(child_path)
|
yield cls(child_path)
|
||||||
|
|
||||||
@ -201,21 +203,23 @@ class Collection(storage.BaseCollection):
|
|||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def exception_cm(path, href=None):
|
def exception_cm(path, href=None):
|
||||||
nonlocal item_errors, collection_errors
|
nonlocal item_errors, collection_errors
|
||||||
|
sane_path = pathutils.strip_path(path)
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if href:
|
if href:
|
||||||
item_errors += 1
|
item_errors += 1
|
||||||
name = "item %r in %r" % (href, path.strip("/"))
|
name = "item %r in %r" % (href, sane_path)
|
||||||
else:
|
else:
|
||||||
collection_errors += 1
|
collection_errors += 1
|
||||||
name = "collection %r" % path.strip("/")
|
name = "collection %r" % sane_path
|
||||||
logger.error("Invalid %s: %s", name, e, exc_info=True)
|
logger.error("Invalid %s: %s", name, e, exc_info=True)
|
||||||
|
|
||||||
remaining_paths = [""]
|
remaining_sane_paths = [""]
|
||||||
while remaining_paths:
|
while remaining_sane_paths:
|
||||||
path = remaining_paths.pop(0)
|
sane_path = remaining_sane_paths.pop(0)
|
||||||
logger.debug("Verifying collection %r", path)
|
path = pathutils.unstrip_path(sane_path, True)
|
||||||
|
logger.debug("Verifying collection %r", sane_path)
|
||||||
with exception_cm(path):
|
with exception_cm(path):
|
||||||
saved_item_errors = item_errors
|
saved_item_errors = item_errors
|
||||||
collection = None
|
collection = None
|
||||||
@ -228,19 +232,20 @@ class Collection(storage.BaseCollection):
|
|||||||
continue
|
continue
|
||||||
if isinstance(item, storage.BaseCollection):
|
if isinstance(item, storage.BaseCollection):
|
||||||
has_child_collections = True
|
has_child_collections = True
|
||||||
remaining_paths.append(item.path)
|
remaining_sane_paths.append(item.path)
|
||||||
elif item.uid in uids:
|
elif item.uid in uids:
|
||||||
cls.logger.error(
|
cls.logger.error(
|
||||||
"Invalid item %r in %r: UID conflict %r",
|
"Invalid item %r in %r: UID conflict %r",
|
||||||
item.href, path.strip("/"), item.uid)
|
item.href, sane_path, item.uid)
|
||||||
else:
|
else:
|
||||||
uids.add(item.uid)
|
uids.add(item.uid)
|
||||||
logger.debug("Verified item %r in %r", item.href, path)
|
logger.debug("Verified item %r in %r",
|
||||||
|
item.href, sane_path)
|
||||||
if item_errors == saved_item_errors:
|
if item_errors == saved_item_errors:
|
||||||
collection.sync()
|
collection.sync()
|
||||||
if has_child_collections and collection.get_meta("tag"):
|
if has_child_collections and collection.get_meta("tag"):
|
||||||
cls.logger.error("Invalid collection %r: %r must not have "
|
cls.logger.error("Invalid collection %r: %r must not have "
|
||||||
"child collections", path.strip("/"),
|
"child collections", sane_path,
|
||||||
collection.get_meta("tag"))
|
collection.get_meta("tag"))
|
||||||
return item_errors == 0 and collection_errors == 0
|
return item_errors == 0 and collection_errors == 0
|
||||||
|
|
||||||
@ -249,12 +254,12 @@ class Collection(storage.BaseCollection):
|
|||||||
folder = cls._get_collection_root_folder()
|
folder = cls._get_collection_root_folder()
|
||||||
|
|
||||||
# Path should already be sanitized
|
# Path should already be sanitized
|
||||||
sane_path = pathutils.sanitize_path(href).strip("/")
|
sane_path = pathutils.strip_path(href)
|
||||||
filesystem_path = pathutils.path_to_filesystem(folder, sane_path)
|
filesystem_path = pathutils.path_to_filesystem(folder, sane_path)
|
||||||
|
|
||||||
if not props:
|
if not props:
|
||||||
cls._makedirs_synced(filesystem_path)
|
cls._makedirs_synced(filesystem_path)
|
||||||
return cls(sane_path)
|
return cls(pathutils.unstrip_path(sane_path, True))
|
||||||
|
|
||||||
parent_dir = os.path.dirname(filesystem_path)
|
parent_dir = os.path.dirname(filesystem_path)
|
||||||
cls._makedirs_synced(parent_dir)
|
cls._makedirs_synced(parent_dir)
|
||||||
@ -265,7 +270,8 @@ class Collection(storage.BaseCollection):
|
|||||||
# The temporary directory itself can't be renamed
|
# The temporary directory itself can't be renamed
|
||||||
tmp_filesystem_path = os.path.join(tmp_dir, "collection")
|
tmp_filesystem_path = os.path.join(tmp_dir, "collection")
|
||||||
os.makedirs(tmp_filesystem_path)
|
os.makedirs(tmp_filesystem_path)
|
||||||
self = cls(sane_path, filesystem_path=tmp_filesystem_path)
|
self = cls(pathutils.unstrip_path(sane_path, True),
|
||||||
|
filesystem_path=tmp_filesystem_path)
|
||||||
self.set_meta(props)
|
self.set_meta(props)
|
||||||
if items is not None:
|
if items is not None:
|
||||||
if props.get("tag") == "VCALENDAR":
|
if props.get("tag") == "VCALENDAR":
|
||||||
@ -281,7 +287,7 @@ class Collection(storage.BaseCollection):
|
|||||||
os.rename(tmp_filesystem_path, filesystem_path)
|
os.rename(tmp_filesystem_path, filesystem_path)
|
||||||
cls._sync_directory(parent_dir)
|
cls._sync_directory(parent_dir)
|
||||||
|
|
||||||
return cls(sane_path)
|
return cls(pathutils.unstrip_path(sane_path, True))
|
||||||
|
|
||||||
def _upload_all_nonatomic(self, items, suffix=""):
|
def _upload_all_nonatomic(self, items, suffix=""):
|
||||||
"""Upload a new set of items.
|
"""Upload a new set of items.
|
||||||
|
@ -19,11 +19,12 @@ Custom rights management.
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from radicale import rights
|
from radicale import pathutils, rights
|
||||||
|
|
||||||
|
|
||||||
class Rights(rights.BaseRights):
|
class Rights(rights.BaseRights):
|
||||||
def authorized(self, user, path, permissions):
|
def authorized(self, user, path, permissions):
|
||||||
if path.strip("/") not in ("tmp", "other"):
|
sane_path = pathutils.strip_path(path)
|
||||||
|
if sane_path not in ("tmp", "other"):
|
||||||
return ""
|
return ""
|
||||||
return rights.intersect_permissions(permissions)
|
return rights.intersect_permissions(permissions)
|
||||||
|
@ -48,9 +48,11 @@ class Web(web.BaseWeb):
|
|||||||
"internal_data")
|
"internal_data")
|
||||||
|
|
||||||
def get(self, environ, base_prefix, path, user):
|
def get(self, environ, base_prefix, path, user):
|
||||||
|
assert path == "/.web" or path.startswith("/.web/")
|
||||||
|
assert pathutils.sanitize_path(path) == path
|
||||||
try:
|
try:
|
||||||
filesystem_path = pathutils.path_to_filesystem(
|
filesystem_path = pathutils.path_to_filesystem(
|
||||||
self.folder, path[len("/.web"):])
|
self.folder, path[len("/.web"):].strip("/"))
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logger.debug("Web content with unsafe path %r requested: %s",
|
logger.debug("Web content with unsafe path %r requested: %s",
|
||||||
path, e, exc_info=True)
|
path, e, exc_info=True)
|
||||||
|
@ -16,11 +16,13 @@
|
|||||||
|
|
||||||
from http import client
|
from http import client
|
||||||
|
|
||||||
from radicale import httputils, web
|
from radicale import httputils, pathutils, web
|
||||||
|
|
||||||
|
|
||||||
class Web(web.BaseWeb):
|
class Web(web.BaseWeb):
|
||||||
def get(self, environ, base_prefix, path, user):
|
def get(self, environ, base_prefix, path, user):
|
||||||
|
assert path == "/.web" or path.startswith("/.web/")
|
||||||
|
assert pathutils.sanitize_path(path) == path
|
||||||
if path != "/.web":
|
if path != "/.web":
|
||||||
return httputils.NOT_FOUND
|
return httputils.NOT_FOUND
|
||||||
return client.OK, {"Content-Type": "text/plain"}, "Radicale works!"
|
return client.OK, {"Content-Type": "text/plain"}, "Radicale works!"
|
||||||
|
@ -33,6 +33,8 @@ from collections import OrderedDict
|
|||||||
from http import client
|
from http import client
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
from radicale import pathutils
|
||||||
|
|
||||||
MIMETYPES = {
|
MIMETYPES = {
|
||||||
"VADDRESSBOOK": "text/vcard",
|
"VADDRESSBOOK": "text/vcard",
|
||||||
"VCALENDAR": "text/calendar"}
|
"VCALENDAR": "text/calendar"}
|
||||||
@ -118,6 +120,7 @@ def make_response(code):
|
|||||||
|
|
||||||
def make_href(base_prefix, href):
|
def make_href(base_prefix, href):
|
||||||
"""Return prefixed href."""
|
"""Return prefixed href."""
|
||||||
|
assert href == pathutils.sanitize_path(href)
|
||||||
return quote("%s%s" % (base_prefix, href))
|
return quote("%s%s" % (base_prefix, href))
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user