id: PYSEC-2026-2554 published: "2026-07-13T15:46:16.729758Z" modified: "2026-07-13T16:04:28.514769Z" aliases: - CVE-2026-48053 - GHSA-4mj9-pf4r-cqrc summary: Kolibri has Unauthenticated Server-Side Request Forgery (SSRF) in RemoteFacilityUserViewset details: "## Summary\n\nSeveral Kolibri API endpoints accept an unvalidated `baseurl` parameter and fetch attacker-controlled URLs from the Kolibri server, reflecting the response body back to the caller. The original report identified two endpoints on the `RemoteFacilityUser*` viewsets; remediation review found two further reflection points on the same pattern. The GET endpoint was unauthenticated.\n\n## Affected endpoints\n\nReported:\n\n- `GET /api/auth/remotefacilityuser` → `RemoteFacilityUserViewset` (`kolibri/core/auth/api.py:1570`). No authentication required.\n- `POST /api/auth/remotefacilityauthenticateduserinfo` → `RemoteFacilityUserAuthenticatedViewset` (`kolibri/core/auth/api.py:1594`). Authentication is checked against the *remote* server rather than the local Kolibri.\n\nFound during remediation:\n\n- `POST /api/public/setupwizard/loddata` → setup wizard's remote-signup proxy (`kolibri/plugins/setup_wizard/api.py`). Reachable on unprovisioned devices.\n- `GET /api/public/networklocation//facilities/` → `NetworkLocationFacilitiesView` (`kolibri/core/discovery/api.py`). Authenticated but with the same `Response(remote_payload)` pattern.\n\n## Root cause\n\nTwo compounding issues:\n\n1. **Response reflection** — these endpoints returned the remote server's JSON body more or less verbatim to the caller (`Response(response.json())`, `Response(facility_info[\"users\"])`, etc.).\n2. **No restriction on the remote target** — `baseurl` was validated only by `URLValidator(schemes=[\"http\", \"https\"])`. `NetworkClient.build_for_address()` would connect to any host with a valid Kolibri-shaped `/api/public/info/` response, and `requests` followed 30x redirects by default, so a hostile peer could pivot the fetch to an arbitrary host (cloud metadata, internal services) before reflection.\n\n## Two reflection vectors\n\n**GET vector (`RemoteFacilityUserViewset`):**\nThe viewset fetched `/api/public/facilitysearchuser/` and returned `Response(response.json())`. An attacker-controlled `baseurl` returned a 302 to an arbitrary internal URL; `requests` followed the redirect, and the redirected response body was returned to the attacker.\n\n**POST vector (`RemoteFacilityUserAuthenticatedViewset`):**\n`get_remote_users_info()` fetched `/api/public/facilityuser/` with Basic Auth and the viewset returned `Response(facility_info[\"users\"])`. A malicious `baseurl` returned crafted user-shaped JSON; arbitrary smuggled fields were reflected back to the caller. The setup wizard and `NetworkLocationFacilitiesView` endpoints had the same shape on different remote URLs.\n\n## Reproduction\n\nThe vulnerability can be reproduced by pointing `baseurl` at an attacker-controlled HTTP server that:\n\n1. Responds to `GET /api/public/info/` with a valid Kolibri info payload (so `NetworkClient.build_for_address()` succeeds).\n2. **GET vector:** responds to `GET /api/public/facilitysearchuser/` with a 302 redirect to the target URL. The redirected response body is reflected via `Response(response.json())`.\n3. **POST vector:** responds to the relevant remote URL with crafted JSON containing additional fields. The full JSON is reflected.\n\nA working PoC has been retained internally and is not published with this advisory.\n\n## Demonstrated impact (pre-fix)\n\n- **Unauthenticated outbound requests from the Kolibri server** to any HTTP(S) URL the attacker chose (GET endpoint only; the others required auth or an unprovisioned device).\n- **Reflected data exfiltration** for any HTTP endpoint that responded to a plain `GET` with JSON and no special request headers.\n- **Cloud metadata reachability** was realistic but service-specific:\n - AWS IMDSv1 — reachable\n - DigitalOcean (`/metadata/v1.json`) — reachable\n - GCP, Azure, AWS IMDSv2 — *not* reachable via this vector (require `Metadata-Flavor` / `Metadata` / token headers that the attacker could not inject)\n- **Reachability of internal HTTP services** on the same network as the Kolibri server, with their JSON responses returned to the attacker.\n\n## Not demonstrated\n\nThe earlier draft asserted port scanning via a timing oracle and generic \"internal network mapping.\" The reflection vector reads response bodies directly when the target speaks JSON; timing-based scanning of arbitrary TCP services was not demonstrated and is not the headline risk.\n\n## Mitigation\n\nFour layers of defence:\n\n1. **Response sanitisation.** Each affected endpoint now coerces the remote response to a documented shape before returning it. Smuggled fields are dropped.\n2. **Authentication.** The previously-open `RemoteFacilityUser*` endpoints now require an authenticated caller (or an unprovisioned device, for setup-wizard flows).\n3. **Cross-host redirect blocking.** Remote-fetch HTTP sessions refuse 30x responses that point to a different hostname. Same-host redirects still work.\n4. **Peer allowlist.** Endpoints that accept a caller-supplied `baseurl` resolve it only to peers Kolibri already knows about, rather than connecting to arbitrary hosts. Discovery and CLI flows that legitimately need to probe new addresses use a separate code path.\n\n## Credit\n\nInitial report and identification of the `RemoteFacilityUser*` viewsets by @beraoudabdelkhalek. Reflection-based PoC, additional vector identification, and remediation by the Kolibri maintainers.\n\n\n
Original report by @beraoudabdelkhalek\n\n### Summary\nThe `RemoteFacilityUserViewset` API endpoint (`/api/auth/remotefacilityuser`) has no authentication or permission checks and accepts a user-controlled `baseurl` parameter. This parameter is passed directly to `NetworkClient.build_for_address()` which makes server-side HTTP requests to the attacker-specified URL. An unauthenticated attacker can force the Kolibri server to reach out to arbitrary internal hosts, port-scan internal networks, and access cloud metadata endpoints.\n\n### Details\n\nThis is mainly due to the following issues:\n\n**1. Missing authentication on the API endpoint**\n\nFile: `kolibri/core/auth/api.py`, line ~1553\n\n```python\nclass RemoteFacilityUserViewset(views.APIView): # No permission_classes → AllowAny\n def get(self, request):\n baseurl = request.query_params.get(\"baseurl\", \"\")\n validator(baseurl) # Only checks URL format (http/https scheme + valid hostname)\n client = NetworkClient.build_for_address(baseurl)\n response = client.get(url, params={\"facility\": facility, \"search\": username})\n```\n\nNo `permission_classes` attribute is defined, and `DEFAULT_PERMISSION_CLASSES` is not set in the DRF configuration, so the endpoint defaults to `AllowAny` , accepting requests with zero authentication.\n\nSimilarly, `RemoteFacilityUserAuthenticatedViewset` (line ~1577, POST endpoint) also has no `permission_classes`, though it currently checks permissions via a different mechanism. The initial `build_for_address()` call still fires before that check.\n\n**2. Weak URL validation**\n\nFile: `kolibri/utils/urls.py`, line 1-7\n\n```python\nfrom django.core.validators import URLValidator\nvalidator = URLValidator(schemes=[\"http\", \"https\"])\n```\n\nThe only validation is that the URL has an http or https scheme and a valid hostname. There is no block on:\n- RFC 1918 private IPs (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)\n- Loopback addresses (127.0.0.0/8, ::1)\n- Link-local addresses (169.254.0.0/16, including AWS/GCP/Azure metadata endpoints)\n- IPv6 equivalents of any of the above\n\n### PoC\n**Prerequisites**: A listener on a host reachable by the Kolibri server (e.g., `nc -lvp 1337`) the listener can be local or remote.\n\nAgainst a local Docker deployment (validated against Kolibri 0.19.3):\n\n```bash\n# Trigger the SSRF no auth headers needed\ncurl \"http://localhost:8080/api/auth/remotefacilityuser?baseurl=http://172.17.0.1:1337&username=test&facility=\"\n```\n\nThe Kolibri server makes an outbound HTTP request to the attacker's listener:\n\n```\nGET /api/public/info/?v=3 HTTP/1.1\nHost: 172.17.0.1:1337\nUser-Agent: Kolibri/0.19.3 python-requests/2.27.1\nAccept-Encoding: gzip, deflate\nAccept: */*\nConnection: keep-alive\n```\n\nTesters have also confirmed the issue against live deployments of Kolibri.\n\n### Impact\nUnauthenticated SSRF : any attacker who can reach the Kolibri server can make it issue HTTP requests to arbitrary hosts, with no credentials needed\nInternal network scanning : the built-in port scanning behavior (5+ ports per HTTP target, 24+ connection attempts per request) allows mapping internal networks through the timing oracle\nCloud metadata access : if Kolibri runs on a cloud VM (AWS EC2, GCP, Azure), the attacker can reach 169.254.169.254 and potentially exfiltrate IAM credentials and instance metadata\nInternal service discovery : other Kolibri instances or internal services on the network can be discovered and their API responses read by the attacker\nBlind SSRF via POST endpoint : RemoteFacilityUserAuthenticatedViewset returns 403 to the attacker but still makes the outbound request before the permission check\n\n
" affected: - package: name: kolibri ecosystem: PyPI purl: pkg:pypi/kolibri ranges: - type: ECOSYSTEM events: - introduced: "0" - fixed: 0.19.4 versions: - 0.0.1.dev20160531112859 - 0.1.1rc2 - 0.1.1rc3 - 0.10.0 - 0.10.1 - 0.10.2 - 0.10.3 - 0.10.3b2 - 0.10.3b3 - 0.11.0 - 0.11.0b2 - 0.11.1 - 0.12.0 - 0.12.0b5 - 0.12.0b6 - 0.12.0b7 - 0.12.0b8 - 0.12.0b9 - 0.12.1 - 0.12.10b3 - 0.12.2 - 0.12.3 - 0.12.3b1 - 0.12.4 - 0.12.4b2 - 0.12.4b3 - 0.12.4b5 - 0.12.4b6 - 0.12.4b7 - 0.12.4b8 - 0.12.5 - 0.12.5b1 - 0.12.5b2 - 0.12.6 - 0.12.6b1 - 0.12.6b2 - 0.12.6rc1 - 0.12.7 - 0.12.8 - 0.12.8rc1 - 0.12.9 - 0.12.9rc1 - 0.13.0 - 0.13.0b2 - 0.13.0b3 - 0.13.0b4 - 0.13.0b5 - 0.13.0b6 - 0.13.0b7 - 0.13.0b8 - 0.13.1 - 0.13.1b1 - 0.13.1b2 - 0.13.2 - 0.13.2b1 - 0.13.3 - 0.13.3b1 - 0.13.3rc1 - 0.14.0 - 0.14.0b10 - 0.14.0b3 - 0.14.0rc1 - 0.14.1 - 0.14.2 - 0.14.3 - 0.14.4 - 0.14.4rc1 - 0.14.5 - 0.14.6 - 0.14.7 - 0.14.8a1 - 0.14.8a2 - 0.14.8a4 - 0.14.8a5 - 0.14.8a6 - 0.14.8a7 - 0.14.8a8 - 0.15.0 - 0.15.0b5 - 0.15.1 - 0.15.10 - 0.15.11 - 0.15.12 - 0.15.1b1 - 0.15.2 - 0.15.3 - 0.15.4 - 0.15.5 - 0.15.6 - 0.15.7 - 0.15.8 - 0.15.9 - 0.16.0 - 0.16.1 - 0.16.2 - 0.17.0 - 0.17.1 - 0.17.2 - 0.17.3 - 0.17.4 - 0.17.5 - 0.18.0 - 0.18.1 - 0.18.2 - 0.18.3 - 0.18.4 - 0.19.0 - 0.19.1 - 0.19.2 - 0.19.3 - 0.2.0a0 - 0.2.0a2 - 0.2.1 - 0.4.4 - 0.5.3 - 0.6.1 - 0.7.0 - 0.7.1 - 0.7.2 - 0.7.2b2 - 0.8.0 - 0.8.0b3 - 0.8.0b4 - 0.9.0 - 0.9.1 - 0.9.2 references: - type: WEB url: https://github.com/learningequality/kolibri/security/advisories/GHSA-4mj9-pf4r-cqrc - type: PACKAGE url: https://github.com/learningequality/kolibri - type: WEB url: https://github.com/learningequality/kolibri/releases/tag/v0.19.4 - type: PACKAGE url: https://pypi.org/project/kolibri - type: ADVISORY url: https://github.com/advisories/GHSA-4mj9-pf4r-cqrc - type: ADVISORY url: https://nvd.nist.gov/vuln/detail/CVE-2026-48053 severity: - type: CVSS_V3 score: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N