Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐍
🐍 PyPI
Not in CISA KEV
MEDIUM severity

GHSA-x287-5c68-36wp

MEDIUMFix: openwisp/openwisp-ipam#220

GHSA-x287-5c68-36wp is a medium-severity (CVSS 5.3) vulnerability in openwisp-ipam. O3 Security confirms whether GHSA-x287-5c68-36wp is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.

OpenWISP IPAM has broken object-level authorization: ExportSubnetView lets a member of one organization export another organization's subnet and all its IP addresses

Published
Aug 26, 2026
Updated
Aug 26, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Aug 26, 2026 · OSV.dev, FIRST.org (EPSS)

Real-World Exposure

1 pkg affected
🐍openwisp-ipam

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.

Description

Summary

OpenWISP IPAM is multi-tenant: every Subnet belongs to an organization, and API access is scoped to the organizations a user belongs to. The CSV export endpoint, ExportSubnetView, omits the organization-membership check that its import sibling performs, and loads the subnet by primary key with no organization filter. An authenticated user who is a member of one organization can therefore export a subnet belonging to another organization — its name, CIDR, organization slug and every IP address in it — by issuing an export request for that subnet's id.

Details

ImportSubnetView.post() authorizes first, by calling assert_organization_permissions():

# openwisp_ipam/api/views.py  (ImportSubnetView)
def post(self, request, *args, **kwargs):
    self.assert_organization_permissions(request)        # <-- org-membership check
    file = request.FILES["csvfile"]
    ...
    self.subnet_model().import_csv(file)
# openwisp_ipam/api/utils.py:16  (AuthorizeCSVImport)
def assert_organization_permissions(self, request):
    if request.user.is_superuser:
        return
    # ... otherwise verifies the user belongs to the target organization

ExportSubnetView.post() performs no such check. It is declared with IpAddressOrgMixin (whose organization scoping lives in get_queryset()), but its overridden post() never calls get_queryset() or assert_organization_permissions() — it goes straight to export_csv():

# openwisp_ipam/api/views.py:246
class ExportSubnetView(ProtectedAPIMixin, IpAddressOrgMixin, CreateAPIView):
    subnet_model = Subnet
    def post(self, request, *args, **kwargs):
        response = HttpResponse(content_type="text/csv")
        response["Content-Disposition"] = 'attachment; filename="ip_address.csv"'
        writer = csv.writer(response)
        self.subnet_model().export_csv(kwargs["subnet_id"], writer)   # no org check reached
        return response

export_csv() resolves the subnet by primary key with no organization filter and writes its full contents:

# openwisp_ipam/base/models.py:242
def export_csv(self, subnet_id, writer):
    ipaddress_model = load_model("openwisp_ipam", "IpAddress")
    subnet = load_model("openwisp_ipam", "Subnet").objects.get(pk=subnet_id)   # any org's subnet
    # ... writes subnet name, CIDR, organization slug, and every IpAddress (address, description, …)

The subnet_id is a random UUIDField (migrations/0001_initial.py: models.UUIDField(default=uuid.uuid4)), so an attacker must obtain the target subnet's id rather than enumerate it — hence Attack Complexity: High. This is a genuine missing-authorization flaw regardless: the import/export asymmetry shows the check was intended, and subnet ids routinely appear in URLs, logs, shared CSV exports and support tickets, from which a cross-organization id can leak.

The same single-object pattern (resolving the subnet by subnet_id outside the org-scoped queryset) also appears in AvailableIpView.get() and RequestIPView.post() (the latter additionally writes an IP into the target subnet); these are worth reviewing alongside the export fix.

Proof of concept

Prerequisites: an OpenWISP IPAM instance with two organizations OrgA and OrgB; a normal (non-superuser) user who is a member of OrgB only; a subnet in OrgA whose id S is known to the attacker (e.g. leaked via a URL/log/shared export).

  1. As the OrgB user, authenticate to the API.
  2. POST /api/v1/subnet/{S}/export/ (OrgA's subnet id).
  3. Result: a CSV download containing OrgA's subnet name, CIDR, organization slug and every IP address in it — even though the user belongs only to OrgB.
  4. Control (proves it's an oversight): the corresponding import endpoint POST /api/v1/import-subnet/ with OrgA data is rejected for the same user by assert_organization_permissions().

(Reported from a first-hand source review of the current master: the missing check in ExportSubnetView.post, the guarded ImportSubnetView.post sibling, the un-filtered Subnet.objects.get(pk=…) in export_csv, and the UUIDField primary key were each verified by hand. The fix is a one-line authorization call, so a maintainer can confirm trivially with two organizations.)

Impact

A member of one organization can read the complete contents of another organization's subnet — its IP allocation inventory (addresses, descriptions, organization slug, CIDR). In a WISP / network-management deployment this discloses another tenant's network topology and host inventory. Exploitation requires obtaining the target subnet's UUID (AC:H), but the authorization check is missing outright. (RequestIPView shares the pattern and additionally allows writing an IP into another organization's subnet — a separate integrity concern for the maintainer to confirm.)

Remediation

Add the organization-membership check to ExportSubnetView.post() — call self.assert_organization_permissions(request) (as ImportSubnetView does), or resolve the subnet through the org-scoped get_queryset() / get_object() and return 404 when it isn't in the caller's organizations. Apply the same to AvailableIpView and RequestIPView.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐍PyPIopenwisp-ipamall versions1.2.1

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for openwisp-ipam. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.

  2. Fix

    Update openwisp-ipam to 1.2.1 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-x287-5c68-36wp is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 pinpoints whether GHSA-x287-5c68-36wp is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.

Tailored to GHSA-x287-5c68-36wp. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

## Summary OpenWISP IPAM is multi-tenant: every `Subnet` belongs to an `organization`, and API access is scoped to the organizations a user belongs to. The CSV **export** endpoint, `ExportSubnetView`, omits the organization-membership check that its **import** sibling performs, and loads the subnet by primary key with no organization filter. An authenticated user who is a member of one organization can therefore export a subnet belonging to **another** organization — its name, CIDR, organization slug and every IP address in it — by issuing an export request for that subnet's id. ## Details
O3 Security · Impact-Aware SCA

Is GHSA-x287-5c68-36wp in your dependencies?

O3 detects GHSA-x287-5c68-36wp across PyPI dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.

GHSA-x287-5c68-36wp: openwisp-ipam… | O3 Security