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
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,5 @@ cython_debug/

.ruff_cache

uv.lock

local_test
crudadmin_data/
9 changes: 9 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@ Stay informed about security updates:
3. Subscribe to our security mailing list
4. Monitor our release notes

## Acknowledgments

We are grateful to the researchers who report vulnerabilities responsibly. Reporters
of fixed issues are listed here with their permission.

| Reporter | Issue | Fixed in |
| -------- | ----- | -------- |
| [Ubaid Ur Rehman](mailto:arifubaid0345@gmail.com), using [shadowaudit](https://gitlab.com/theredhacker0345/shadowaudit) | Authentication bypass in `AdminAuthMiddleware`: the login-page and static-asset exemptions matched by path suffix and substring, so a crafted URL could reach admin read endpoints without a session | v0.5.1 |

## License

This security policy is part of the CRUDAdmin project and is subject to the same license terms.
4 changes: 2 additions & 2 deletions crudadmin/admin_interface/admin_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,15 +201,15 @@ def setup_routes(self) -> None:
self.dashboard_content(),
methods=["GET"],
include_in_schema=False,
dependencies=[Depends(self.admin_authentication.get_current_user)],
dependencies=[Depends(self.admin_authentication.get_current_user())],
response_model=None,
)
self.router.add_api_route(
"/",
self.dashboard_page(),
methods=["GET"],
include_in_schema=False,
dependencies=[Depends(self.admin_authentication.get_current_user)],
dependencies=[Depends(self.admin_authentication.get_current_user())],
response_model=None,
)

Expand Down
21 changes: 13 additions & 8 deletions crudadmin/admin_interface/auth.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from typing import Optional
from typing import Any, Callable, Optional

from fastapi import Cookie, Depends, Request
from fastapi.security import OAuth2PasswordBearer
Expand Down Expand Up @@ -39,6 +39,7 @@ def __init__(
self.auth_models = {}
self.event_integration = event_integration
self.session_manager = session_manager
self._current_user_dependency: Optional[Callable[..., Any]] = None

self.auth_models[self.db_config.AdminUser.__name__] = {
"model": self.db_config.AdminUser,
Expand All @@ -58,7 +59,16 @@ def __init__(
"delete_schema": None,
}

def get_current_user(self):
def get_current_user(self) -> Callable[..., Any]:
"""Return the dependency that resolves the session cookie to an admin user.

The dependency is built once and reused, so routes that declare it at more
than one level (router and route) share a single callable and FastAPI's
dependency cache resolves it once per request.
"""
if self._current_user_dependency is not None:
return self._current_user_dependency

async def get_current_user_inner(
request: Request,
db: AsyncSession = Depends(self.db_config.get_admin_db),
Expand All @@ -67,12 +77,6 @@ async def get_current_user_inner(
if not session_id:
raise UnauthorizedException("Not authenticated")

is_valid_session = await self.session_manager.validate_session(
session_id=session_id
)
if not is_valid_session:
raise UnauthorizedException("Could not validate credentials")

session_data = await self.session_manager.validate_session(
session_id=session_id
)
Expand All @@ -93,6 +97,7 @@ async def get_current_user_inner(
logger.debug("User not found")
raise UnauthorizedException("User not authenticated")

self._current_user_dependency = get_current_user_inner
return get_current_user_inner

async def get_current_superuser(self, current_user: AdminUserRead) -> AdminUserRead:
Expand Down
8 changes: 2 additions & 6 deletions crudadmin/admin_interface/crud_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -846,9 +846,7 @@ def setup(
allowed_actions=allowed_actions,
)

get_user_dependency = cast(
Callable[..., AsyncSession], self.admin_authentication.get_current_user
)
get_user_dependency = self.admin_authentication.get_current_user()

self.router.add_api_route(
"/management/health",
Expand Down Expand Up @@ -1162,9 +1160,7 @@ class Config:
if self.track_events and self.event_integration:
admin_view.event_integration = self.event_integration

current_user_dep = cast(
Callable[..., Any], self.admin_site.admin_authentication.get_current_user
)
current_user_dep = self.admin_site.admin_authentication.get_current_user()
self.app.include_router(
admin_view.router,
prefix=f"/{model_key}",
Expand Down
16 changes: 10 additions & 6 deletions crudadmin/admin_interface/middleware/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ async def dispatch(self, request: Request, call_next):
if not request.url.path.startswith(expected_prefix):
return await call_next(request)

is_login_path = request.url.path.endswith("/login")
is_static_path = "/static/" in request.url.path
url_prefix = self.admin_instance.get_url_prefix()

is_login_path = request.url.path.rstrip("/") == f"{url_prefix}/login"
is_static_path = request.url.path.startswith(f"{url_prefix}/static/")

if is_login_path or is_static_path:
response = await call_next(request)
Expand All @@ -63,7 +65,9 @@ async def dispatch(self, request: Request, call_next):

if not session_id:
logger.debug("Missing session_id")
login_url = f"{self.admin_instance.get_url_prefix()}/login?error=Please+log+in+to+access+this+page"
login_url = (
f"{url_prefix}/login?error=Please+log+in+to+access+this+page"
)
return RedirectResponse(
url=login_url,
status_code=303,
Expand All @@ -78,7 +82,7 @@ async def dispatch(self, request: Request, call_next):

if not session_data:
logger.debug("Invalid or expired session")
login_url = f"{self.admin_instance.get_url_prefix()}/login?error=Session+expired"
login_url = f"{url_prefix}/login?error=Session+expired"
return RedirectResponse(
url=login_url,
status_code=303,
Expand All @@ -91,7 +95,7 @@ async def dispatch(self, request: Request, call_next):

if not user:
logger.debug("User not found for session")
login_url = f"{self.admin_instance.get_url_prefix()}/login?error=User+not+found"
login_url = f"{url_prefix}/login?error=User+not+found"
return RedirectResponse(
url=login_url,
status_code=303,
Expand All @@ -115,7 +119,7 @@ async def dispatch(self, request: Request, call_next):
or "/crud/" in request.url.path
):
raise
login_url = f"{self.admin_instance.get_url_prefix()}/login?error=Authentication+error"
login_url = f"{url_prefix}/login?error=Authentication+error"
return RedirectResponse(
url=login_url,
status_code=303,
Expand Down
16 changes: 15 additions & 1 deletion crudadmin/admin_interface/model_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,9 @@ def setup_routes(self) -> None:
- Update: /update/{id} (GET), /form_update/{id} (POST)

Routes are configured based on the allowed_actions set provided during initialization.
All routes use appropriate templates and include required dependencies.
All routes use appropriate templates and include required dependencies. Every route
declares an authentication dependency in addition to the admin auth middleware, so a
request that reaches an endpoint without a valid session is rejected there as well.

Example:
```python
Expand All @@ -541,6 +543,12 @@ def setup_routes(self) -> None:
view.setup_routes() # Only creates view/create/update routes
```
"""
auth_dependencies = (
[Depends(self.admin_site.admin_authentication.get_current_user())]
if self.admin_site is not None
else []
)

if "create" in self.allowed_actions:
self.router.add_api_route(
"/form_create",
Expand All @@ -554,6 +562,7 @@ def setup_routes(self) -> None:
self.get_model_create_page(template="admin/model/create.html"),
methods=["GET"],
include_in_schema=False,
dependencies=auth_dependencies,
response_model=None,
)

Expand All @@ -563,6 +572,7 @@ def setup_routes(self) -> None:
self.get_model_admin_page(),
methods=["GET"],
include_in_schema=False,
dependencies=auth_dependencies,
response_model=None,
)
self.router.add_api_route(
Expand All @@ -572,6 +582,7 @@ def setup_routes(self) -> None:
),
methods=["GET"],
include_in_schema=False,
dependencies=auth_dependencies,
response_model=None,
)

Expand All @@ -590,6 +601,7 @@ def setup_routes(self) -> None:
self.get_model_update_page(template="admin/model/update.html"),
methods=["GET"],
include_in_schema=False,
dependencies=auth_dependencies,
response_model=None,
)
self.router.add_api_route(
Expand All @@ -606,13 +618,15 @@ def setup_routes(self) -> None:
self.get_related_data_endpoint(),
methods=["GET"],
include_in_schema=False,
dependencies=auth_dependencies,
response_model=None,
)
self.router.add_api_route(
"/relationship-options/{relationship_name}",
self.get_relationship_options_endpoint(),
methods=["GET"],
include_in_schema=False,
dependencies=auth_dependencies,
response_model=None,
)

Expand Down
1 change: 1 addition & 0 deletions docs/community/SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--8<-- "SECURITY.md"
8 changes: 8 additions & 0 deletions docs/community/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Welcome to the project's community hub. Here, you'll find essential resources an
## Table of Contents
- [Contributing](#contributing)
- [Code of Conduct](#code-of-conduct)
- [Security Policy](#security-policy)
- [License](#license)

---
Expand All @@ -23,6 +24,13 @@ The Code of Conduct outlines the standards and behaviors expected of our communi

---

## Security Policy
[View the Security Policy](SECURITY.md)

The security policy explains which versions receive security updates and how to report a vulnerability privately. If you believe you have found a security issue, please follow this process rather than opening a public issue. It also acknowledges the researchers whose reports have led to fixes.

---

## License
[View the License](LICENSE.md)

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "crudadmin"
version = "0.5.0"
version = "0.5.1"
description = "FastAPI-based admin interface with authentication, event logging and CRUD operations"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
Loading
Loading