#23195·netbox

Reflected XSS: IPAddressAssignView puts an unvalidated return_url into the Cancel link

Author: arthansonCreated Sep 17, 2026Updated Sep 17, 2026
Labelstype: bugstatus: acceptedseverity: mediumnetbox

NetBox Edition

NetBox Community

NetBox Version

v4.7.1

Python Version

3.12

Steps to Reproduce

  1. Log in as any user with the ipam.view_ipaddress permission.
  2. Visit /ipam/ip-addresses/assign/?interface=1&return_url=javascript:alert(document.cookie).
  3. Click the Cancel button.

Expected Behavior

Cancel navigates back to a safe in-app page. A return_url that isn't a relative path or an http(s) URL on this host is ignored and replaced with a sensible default, the same way every other view handles it.

Observed Behavior

Cancel runs the attacker's JavaScript in the victim's session. From there the usual escalation applies — read the CSRF token, then act as the victim against the API.

Suspected Cause

IPAddressAssignView (netbox/ipam/views.py:1237) extends generic.ObjectView, which doesn't include GetReturnURLMixin. Both handlers read the parameter straight from the query string and hand it to the template:

  • netbox/ipam/views.py:1256 (get)
  • netbox/ipam/views.py:1273 (post)

The template writes it into an href at netbox/templates/ipam/ipaddress_assign.html:28. Django's autoescaping stops you from breaking out of the attribute, but it doesn't touch the URL scheme, so javascript: goes through untouched.

This is the only view that skips the check. Everywhere else the value goes through GetReturnURLMixin.get_return_url() (netbox/utilities/views.py:162), which calls safe_for_redirect() (netbox/utilities/request.py:124) and rejects anything that isn't http(s) on an allowed host. The other two raw reads, at netbox/netbox/views/generic/object_views.py:337 and :629, both validate the assembled URL before redirecting. There's no site-wide CSP that would block the payload — the only Content-Security-Policy header in the tree is on the attachment view (netbox/netbox/views/misc.py:162).

Proposed Fix

Add GetReturnURLMixin to the view and call self.get_return_url(request) in both get() and post().