Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
- Test with Python 3.13
- Use ruff instead of black/isort

## 4.3.6 / Unreleased

- [#365](https://github.com/mar10/wsgidav/pull/366)
CORS: Access-Control-Expose-Headers is sent on the preflight response instead of the actual response (@padawan)

## 4.3.5 / 2026-06-27

- Fix Blind SQL injection in WsgiDAV MySQL provider [CVE-2026-55509](https://github.com/mar10/wsgidav/security/advisories/GHSA-p6gw-4frg-j7jw)
Expand Down
87 changes: 87 additions & 0 deletions tests/test_cors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# (c) 2009-2024 Martin Wendt and contributors; see WsgiDAV https://github.com/mar10/wsgidav
# Licensed under the MIT license:
# http://www.opensource.org/licenses/mit-license.php
"""
Unit tests for the CORS middleware (wsgidav.mw.cors).

Uses webtest.TestApp to send fake requests through the WSGI stack.
"""

import unittest

import pytest

from tests.util import create_test_folder
from wsgidav.fs_dav_provider import FilesystemProvider
from wsgidav.wsgidav_app import WsgiDAVApp

try:
import webtest
except ImportError:
raise pytest.skip(
"Skip tests that require WebTest", allow_module_level=True
) from None

ORIGIN = "https://example.org"


class CorsTest(unittest.TestCase):
"""Test the CORS middleware header placement."""

def setUp(self):
self.root_path = create_test_folder("wsgidav-cors-test")
provider = FilesystemProvider(self.root_path)
config = {
"provider_mapping": {"/": provider},
"http_authenticator": {"domain_controller": None},
"simple_dc": {"user_mapping": {"*": True}}, # anonymous access
# "verbose": 1, # changing the log level may break subsequent logger tests
"logging": {"enable_loggers": []},
"property_manager": None,
"lock_storage": True,
"cors": {
"allow_origin": "*",
"allow_methods": "GET, HEAD, OPTIONS, PROPFIND",
"allow_headers": "Authorization, Content-Type, Depth",
"expose_headers": "WWW-Authenticate",
"allow_credentials": True,
},
}
self.app = webtest.TestApp(WsgiDAVApp(config))

def tearDown(self):
del self.app

def test_expose_headers_on_actual_response(self):
"""`Access-Control-Expose-Headers` must be sent on the actual response.

Per the Fetch standard it applies to a CORS request that is *not* a
preflight request, so cross-origin script can read the listed header.
"""
res = self.app.get("/", headers={"Origin": ORIGIN}, status=200)
self.assertEqual(res.headers.get("Access-Control-Allow-Origin"), "*")
self.assertEqual(
res.headers.get("Access-Control-Expose-Headers"),
"WWW-Authenticate",
"Access-Control-Expose-Headers must be present on the actual response",
)

def test_expose_headers_not_on_preflight(self):
"""`Access-Control-Expose-Headers` is meaningless on the preflight.

The preflight only carries Allow-Methods / Allow-Headers / Max-Age.
"""
res = self.app.options(
"/",
headers={
"Origin": ORIGIN,
"Access-Control-Request-Method": "PROPFIND",
},
status="*",
)
# Sanity: this really is a handled preflight.
self.assertIsNotNone(res.headers.get("Access-Control-Allow-Methods"))
self.assertIsNone(
res.headers.get("Access-Control-Expose-Headers"),
"Access-Control-Expose-Headers must not be sent on the preflight",
)
59 changes: 0 additions & 59 deletions tests/test_logging

This file was deleted.

5 changes: 5 additions & 0 deletions tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@ def testDefault(self):
"""By default, there should be no logging."""
_baseLogger = logging.getLogger(BASE_LOGGER_NAME)

# If this fails, some previous test probably changed the default logging level.
assert _baseLogger.getEffectiveLevel() == logging.INFO, (
"Default base logger level should be INFO"
)

_baseLogger.debug("_baseLogger.debug")
_baseLogger.info("_baseLogger.info")
_baseLogger.warning("_baseLogger.warning")
Expand Down
4 changes: 3 additions & 1 deletion wsgidav/mw/cors.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ def __init__(self, wsgidav_app, next_app, config):

add_non_preflight = add_always[:]
if expose_headers:
add_always.append(("Access-Control-Expose-Headers", expose_headers))
add_non_preflight.append(
("Access-Control-Expose-Headers", expose_headers)
)

add_preflight = add_always[:]
if allow_headers:
Expand Down
Loading