Vulnerabilities in pkg:pypi/kolibri

PACKAGE
Advisory Identifier: GHSA-4mj9-pf4r-cqrc
Advisory Summary

CVE-2026-48053 is a MODERATE (CVSS 5.8) kolibri has unauthenticated server-side request vulnerability (CWE-918: Server-Side Request Forgery (SSRF)) affecting Kolibri libraries in the PyPI ecosystem. The issue involves kolibri has unauthenticated server-side request forgery (ssrf) in remotefacilityuserviewset. Remediation is available by upgrading affected packages to version 0.19.4.

1 Known Advisory Record(s)
Package Vulnerability Intelligence

pkg:pypi/kolibri

1 Known Advisory Record(s)

Vulnerability advisories affecting package pkg:pypi/kolibri. Select an advisory below to view technical details or fixes.

Advisory ID CVE / NVD Severity Summary
GHSA-4mj9-pf4r-cqrc CVE-2026-48053 MODERATE Kolibri has Unauthenticated Server-Side Request Forgery (SSRF) in RemoteFacilityUserViewset

CVE-2026-48053 at a Glance

CVE Identifier CVE-2026-48053
GHSA Identifier GHSA-4mj9-pf4r-cqrc
Severity MODERATE
CVSS v3.1 Base Score 5.8
CWE Classification CWE-918: Server-Side Request Forgery (SSRF)
Affected Product Kolibri
Package Ecosystem PyPI
Published Date
Last Updated

Security Impact

CVSS: 5.8

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Changed (S:C)
Confidentiality Low (C:L)
Integrity None (I:N)
Availability None (A:N)

How to Fix CVE-2026-48053

To remediate this vulnerability, upgrade all affected Kolibri dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

kolibri fixed: 0.19.4
pip install kolibri>=0.19.4
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show kolibri

How to Check if You Are Affected

To determine whether your Python environment is affected by CVE-2026-48053, inspect your installed packages using pip:

pip show kolibri

Analyze the terminal output to locate any version of Kolibri in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.19.4), your application is affected.

Note: The pip show kolibri command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-918: Server-Side Request Forgery (SSRF), where the web server receives a url or similar request from an upstream component and retrieves the contents of this url, but it does not sufficiently ensure that the request is being sent to the expected destination. In Kolibri, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Summary Several 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. ## Affected endpoints Reported: - `GET /api/auth/remotefacilityuser` → `RemoteFacilityUserViewset` (`kolibri/core/auth/api.py:1570`). No authentication required. - `POST /api/auth/remotefacilityauthenticateduserinfo` → `RemoteFacilityUserAuthenticatedViewset` (`kolibri/core/auth/api.py:1594`). Authentication is checked against the *remote* server rather than the local Kolibri. Found during remediation: - `POST /api/public/setupwizard/loddata` → setup wizard's remote-signup proxy (`kolibri/plugins/setup_wizard/api.py`). Reachable on unprovisioned devices. - `GET /api/public/networklocation/<id>/facilities/` → `NetworkLocationFacilitiesView` (`kolibri/core/discovery/api.py`). Authenticated but with the same `Response(remote_payload)` pattern. ## Root cause Two compounding issues: 1. **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.). 2. **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. ## Two reflection vectors **GET vector (`RemoteFacilityUserViewset`):** The viewset fetched `<baseurl>/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. **POST vector (`RemoteFacilityUserAuthenticatedViewset`):** `get_remote_users_info()` fetched `<baseurl>/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. ## Reproduction The vulnerability can be reproduced by pointing `baseurl` at an attacker-controlled HTTP server that: 1. Responds to `GET /api/public/info/` with a valid Kolibri info payload (so `NetworkClient.build_for_address()` succeeds). 2. **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())`. 3. **POST vector:** responds to the relevant remote URL with crafted JSON containing additional fields. The full JSON is reflected. A working PoC has been retained internally and is not published with this advisory. ## Demonstrated impact (pre-fix) - **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). - **Reflected data exfiltration** for any HTTP endpoint that responded to a plain `GET` with JSON and no special request headers. - **Cloud metadata reachability** was realistic but service-specific: - AWS IMDSv1 — reachable - DigitalOcean (`/metadata/v1.json`) — reachable - GCP, Azure, AWS IMDSv2 — *not* reachable via this vector (require `Metadata-Flavor` / `Metadata` / token headers that the attacker could not inject) - **Reachability of internal HTTP services** on the same network as the Kolibri server, with their JSON responses returned to the attacker. ## Not demonstrated The 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. ## Mitigation Four layers of defence: 1. **Response sanitisation.** Each affected endpoint now coerces the remote response to a documented shape before returning it. Smuggled fields are dropped. 2. **Authentication.** The previously-open `RemoteFacilityUser*` endpoints now require an authenticated caller (or an unprovisioned device, for setup-wizard flows). 3. **Cross-host redirect blocking.** Remote-fetch HTTP sessions refuse 30x responses that point to a different hostname. Same-host redirects still work. 4. **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. ## Credit Initial report and identification of the `RemoteFacilityUser*` viewsets by @beraoudabdelkhalek. Reflection-based PoC, additional vector identification, and remediation by the Kolibri maintainers. <details><summary>Original report by @beraoudabdelkhalek</summary> ### Summary The `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. ### Details This is mainly due to the following issues: **1. Missing authentication on the API endpoint** File: `kolibri/core/auth/api.py`, line ~1553 ```python class RemoteFacilityUserViewset(views.APIView): # No permission_classes → AllowAny def get(self, request): baseurl = request.query_params.get("baseurl", "") validator(baseurl) # Only checks URL format (http/https scheme + valid hostname) client = NetworkClient.build_for_address(baseurl) response = client.get(url, params={"facility": facility, "search": username}) ``` No `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. Similarly, `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. **2. Weak URL validation** File: `kolibri/utils/urls.py`, line 1-7 ```python from django.core.validators import URLValidator validator = URLValidator(schemes=["http", "https"]) ``` The only validation is that the URL has an http or https scheme and a valid hostname. There is no block on: - RFC 1918 private IPs (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) - Loopback addresses (127.0.0.0/8, ::1) - Link-local addresses (169.254.0.0/16, including AWS/GCP/Azure metadata endpoints) - IPv6 equivalents of any of the above ### PoC **Prerequisites**: A listener on a host reachable by the Kolibri server (e.g., `nc -lvp 1337`) the listener can be local or remote. Against a local Docker deployment (validated against Kolibri 0.19.3): ```bash # Trigger the SSRF no auth headers needed curl "http://localhost:8080/api/auth/remotefacilityuser?baseurl=http://172.17.0.1:1337&username=test&facility=<facility_id>" ``` The Kolibri server makes an outbound HTTP request to the attacker's listener: ``` GET /api/public/info/?v=3 HTTP/1.1 Host: 172.17.0.1:1337 User-Agent: Kolibri/0.19.3 python-requests/2.27.1 Accept-Encoding: gzip, deflate Accept: */* Connection: keep-alive ``` Testers have also confirmed the issue against live deployments of Kolibri. ### Impact Unauthenticated SSRF : any attacker who can reach the Kolibri server can make it issue HTTP requests to arbitrary hosts, with no credentials needed Internal 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 Cloud 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 Internal service discovery : other Kolibri instances or internal services on the network can be discovered and their API responses read by the attacker Blind SSRF via POST endpoint : RemoteFacilityUserAuthenticatedViewset returns 403 to the attacker but still makes the outbound request before the permission check </details>

CVE-2026-48053 Affected Kolibri Versions

1 component(s)
Fixed Versions:
0.19.4
Product / Package Ecosystem Affected Versions Fixed Version
kolibri
PyPI
< 0.19.4
0.19.4

References

Frequently Asked Questions

What is CVE-2026-48053?
CVE-2026-48053 is a security vulnerability in Kolibri involving Kolibri has Unauthenticated Server-Side Request Forgery (SSRF) in RemoteFacilityUserViewset under CWE-918: Server-Side Request Forgery (SSRF).
Which Kolibri versions are affected?
Affected versions include releases prior to 0.19.4 across affected packages: kolibri.
What version fixes CVE-2026-48053?
Upgrading to 0.19.4 (or the latest patched release for your respective branch) resolves the vulnerability.
Is kolibri affected?
Yes, kolibri is affected in releases prior to 0.19.4. Upgrade your dependency to 0.19.4 or later.
What is the CVSS score of CVE-2026-48053?
CVE-2026-48053 has a CVSS v3.1 base score of 5.8 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N.
What CWE is associated with CVE-2026-48053?
CVE-2026-48053 is classified under CWE-918: Server-Side Request Forgery (SSRF) (The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.).
How can I check PyPI dependencies for CVE-2026-48053?
Run the dependency tree command: 'pip show kolibri'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-4mj9-pf4r-cqrc",
  "modified" : "2026-06-11T17:10:33Z",
  "published" : "2026-06-11T17:10:33Z",
  "aliases" : [ "CVE-2026-48053" ],
  "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/<id>/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 `<baseurl>/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 `<baseurl>/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<details><summary>Original report by @beraoudabdelkhalek</summary>\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=<facility_id>\"\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</details>",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "kolibri"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "0.19.4"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 0.19.3"
    }
  } ],
  "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"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-918" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-06-11T17:10:33Z",
    "nvd_published_at" : null
  }
}