mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-19 01:25:06 +00:00
security: Fixed missing network_access_allowed checks in the VOD proxy. stream_vod, head_vod, stream_xc_movie, and stream_xc_episode were not checking the STREAMS network policy, unlike the equivalent TS proxy endpoints.
This commit is contained in:
parent
fa9a7868ff
commit
c3fb9c0073
2 changed files with 24 additions and 0 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -9,7 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Security
|
||||
|
||||
- Set `DEFAULT_PERMISSION_CLASSES` to `IsAdmin` in the DRF configuration. All viewsets and function-based views that require non-admin or unauthenticated access were explicitly annotated: proxy streaming endpoints (`stream_ts`, `stream_xc`, `stream_vod`, `head_vod`, `stream_xc_movie`, `stream_xc_episode`) use `@permission_classes([AllowAny])` (access is controlled by the per-stream-type network allow-list inside the view body); the `UserAgentViewSet`, `StreamProfileViewSet`, `CoreSettingsViewSet`, and `ProxySettingsViewSet` gained `get_permissions()` methods mapping read actions to `IsStandardUser` and write actions to `IsAdmin`; and `AuthViewSet.logout` was updated to return `[Authenticated()]`.
|
||||
- Fixed missing `network_access_allowed` checks in the VOD proxy. `stream_vod`, `head_vod`, `stream_xc_movie`, and `stream_xc_episode` were not checking the `STREAMS` network policy, unlike the equivalent TS proxy endpoints.
|
||||
- Explicitly marked the HDHomeRun discovery endpoints (`DiscoverAPIView`, `LineupAPIView`, `LineupStatusAPIView`, `HDHRDeviceXMLAPIView`) and the version endpoint with `permission_classes = [AllowAny]` to document their intentionally public access now that the global default is `IsAdmin`.
|
||||
- Fixed path traversal vulnerability in file uploads. The M3U account upload (`apps/m3u/api_views.py`), logo upload (`apps/channels/api_views.py`), and backup upload (`apps/backups/api_views.py`) all used the uploaded filename directly without sanitization. `os.path.join()` discards all preceding components when it encounters an absolute path segment, and `pathlib`'s `/` operator behaves identically; a relative `../` sequence also escapes via OS path resolution at `open()` time. All three upload paths now strip directory components via `Path(name).name` and validate the resolved path remains within the intended upload directory. Exploiting any of these required admin credentials.
|
||||
- Prevented users from setting `xc_password` (and other admin-managed keys) on their own account via the `PATCH /api/accounts/users/me/` endpoint.
|
||||
- Hardened the HLS proxy `change_stream` endpoint by converting it from a plain Django view to a DRF `@api_view` with `@permission_classes([IsAdmin])`, ensuring the endpoint actually enforces admin-only access. The previous decorator arrangement (`@csrf_exempt` + `@permission_classes`) had no effect on a plain Django view.
|
||||
- Added rate limiting to the login endpoint (`POST /api/accounts/token/`) using DRF's built-in throttling. A `LoginRateThrottle` (3 requests/minute per IP, sliding window) is applied to the `TokenObtainPairView`. Repeated failed attempts from the same IP receive `429 Too Many Requests`.
|
||||
- Removed `CORS_ALLOW_CREDENTIALS = True` from CORS configuration. Dispatcharr authenticates via JWT `Authorization` headers and API keys — not cookies — so credentials are never sent cross-origin by browsers. The setting was also redundant: browsers reject `Access-Control-Allow-Credentials: true` when `Access-Control-Allow-Origin` is a wildcard (`*`), so it had no effect in practice.
|
||||
- Updated frontend npm dependencies to resolve 6 audit vulnerabilities (6 high):
|
||||
- Updated `@xmldom/xmldom` 0.8.11 → 0.8.12, resolving **high** XML injection via unsafe CDATA serialization allowing attacker-controlled markup insertion ([GHSA-wh4c-j3r5-mjhp](https://github.com/advisories/GHSA-wh4c-j3r5-mjhp))
|
||||
- Updated `lodash` 4.17.23 → 4.18.1, resolving **high** Code Injection via `_.template` imports key names ([GHSA-r5fr-rjxr-66jc](https://github.com/advisories/GHSA-r5fr-rjxr-66jc)) and **high** Prototype Pollution via array path bypass in `_.unset` and `_.omit` ([GHSA-f23m-r3pf-42rh](https://github.com/advisories/GHSA-f23m-r3pf-42rh))
|
||||
- Updated `vite` 7.3.1 → 7.3.2, resolving **high** Path Traversal in optimized deps `.map` handling ([GHSA-4w7w-66w2-5vf9](https://github.com/advisories/GHSA-4w7w-66w2-5vf9)), **high** `server.fs.deny` bypass with queries ([GHSA-v2wj-q39q-566r](https://github.com/advisories/GHSA-v2wj-q39q-566r)), and **high** Arbitrary File Read via dev server WebSocket ([GHSA-p9ff-h696-f583](https://github.com/advisories/GHSA-p9ff-h696-f583))
|
||||
|
||||
### Removed
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from rest_framework.permissions import AllowAny
|
|||
from apps.accounts.models import User
|
||||
from apps.accounts.permissions import IsAdmin
|
||||
from apps.proxy.utils import check_user_stream_limits
|
||||
from dispatcharr.utils import network_access_allowed
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -303,6 +304,9 @@ def stream_vod(request, content_type, content_id, session_id=None, profile_id=No
|
|||
session_id: Optional session ID from URL path (for persistent connections)
|
||||
profile_id: Optional M3U profile ID for authentication
|
||||
"""
|
||||
if not network_access_allowed(request, "STREAMS"):
|
||||
return JsonResponse({"error": "Forbidden"}, status=403)
|
||||
|
||||
logger.info(f"[VOD-REQUEST] Starting VOD stream request: {content_type}/{content_id}, session: {session_id}, profile: {profile_id}")
|
||||
logger.info(f"[VOD-REQUEST] Full request path: {request.get_full_path()}")
|
||||
logger.info(f"[VOD-REQUEST] Request method: {request.method}")
|
||||
|
|
@ -509,6 +513,9 @@ def head_vod(request, content_type, content_id, session_id=None, profile_id=None
|
|||
|
||||
Returns content length and session URL header for subsequent GET requests
|
||||
"""
|
||||
if not network_access_allowed(request, "STREAMS"):
|
||||
return JsonResponse({"error": "Forbidden"}, status=403)
|
||||
|
||||
logger.info(f"[VOD-HEAD] HEAD request: {content_type}/{content_id}, session: {session_id}, profile: {profile_id}")
|
||||
|
||||
try:
|
||||
|
|
@ -992,6 +999,9 @@ def stop_vod_client(request):
|
|||
@api_view(["GET"])
|
||||
@permission_classes([AllowAny])
|
||||
def stream_xc_movie(request, username, password, stream_id, extension):
|
||||
if not network_access_allowed(request, "STREAMS"):
|
||||
return JsonResponse({"error": "Forbidden"}, status=403)
|
||||
|
||||
from apps.vod.models import M3UMovieRelation
|
||||
|
||||
session_id = request.GET.get('session_id')
|
||||
|
|
@ -1023,6 +1033,9 @@ def stream_xc_movie(request, username, password, stream_id, extension):
|
|||
@api_view(["GET"])
|
||||
@permission_classes([AllowAny])
|
||||
def stream_xc_episode(request, username, password, stream_id, extension):
|
||||
if not network_access_allowed(request, "STREAMS"):
|
||||
return JsonResponse({"error": "Forbidden"}, status=403)
|
||||
|
||||
from apps.vod.models import M3UEpisodeRelation
|
||||
|
||||
session_id = request.GET.get('session_id')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue