
Read-only audit tooling for CVE-2026-0300 (PAN-OS User-ID Authentication Portal exposure)
Read-only audit tooling for CVE-2026-0300 — the unauthenticated buffer overflow in the PAN-OS User-ID Authentication Portal (formerly known as "Captive Portal") on PA-Series and VM-Series firewalls.
This repo accompanies Tailwind Resource Group security advisory TRG-SA-2026-003 and provides:
audit_portal.py — Tier 1: single-firewall auditaudit_fleet.py — Tier 2: Panorama-driven fleet sweep (concurrent)panos_audit.py — shared library used by both CLIsThe scripts are read-only. They issue show-style queries against the PAN-OS XML API and never modify configuration. The API key is sent via the X-PAN-KEY HTTP header rather than as a URL query parameter, so it does not appear in PAN management-plane access logs, TLS-inspecting proxy logs, or the auditor's process arguments.
The CVE record uses the modern feature name "User-ID Authentication Portal." The PAN-OS XML schema, CLI knobs, and documentation URL paths still use the legacy vocabulary. Operators auditing fleets with will silently miss every config because that string never appears as a configuration element. This tooling searches the schema-correct element name regardless of when the config was authored.
captive-portalshow config running | match authentication-portalThe full Tailwind Analysis on this naming gotcha is in TRG-SA-2026-003, "Captive Portal's legacy footprint."
pip install required--insecure is provided for one-shot bring-up audits onlyexport PAN_API_KEY="<key>"
./audit_portal.py --hostname fw1.example.com
Example table output:
vsys: vsys1
configured: True
enabled: True
redirect_host: auth-portal.internal.example.com
mode: redirect
bound: ethernet1/3(internal-trust), ethernet1/4(guest)
tls_verified: True
risk: EXPOSED_LIKELY
Other output formats: --output json or --output csv.
Other flags: --vsys <name> for single-vsys audit, --timeout/--retries for slow upstreams, --debug for verbose error details on stderr.
export PAN_API_KEY="<panorama-key>"
./audit_fleet.py --hostname panorama.example.com --workers 16 --output csv > fleet-audit.csv
Each row in the CSV is one device-vsys with portal status, redirect host, mode, interface bindings, and a risk classification. The script uses Panorama's target=<serial> API parameter so it never connects to managed firewalls directly — one connection to Panorama sweeps the whole fleet.
Notable flags:
--workers N — concurrent audit threads (default 8, max 32). A 1000-device fleet completes in minutes rather than the hour-plus a serial sweep would take.--timeout, --retries — per-API-call settings. Transient URLError and HTTP 5xx are retried with bounded backoff.--exclude-disconnected — by default, disconnected devices are included with risk=DISCONNECTED so they remain visible during the patch window. Use this flag to drop them.--min-expected-devices N — warn (stderr) if Panorama returns fewer devices than N. Useful to detect response truncation against a known fleet size.--serial-filter '0007*' — glob-filter by serial to audit a subset.--debug — print verbose per-device error details to stderr (includes upstream reason strings; off by default to avoid leaking hostnames into customer-facing CSVs).The risk column in output is one of:
| Label | Meaning |
|---|---|
INFO_NOT_CONFIGURED | No <captive-portal> block on the device. Out of scope for CVE-2026-0300. |
INFO_DISABLED | Portal is configured but enabled=no. Vulnerable code path is not active. |
REVIEW_POLICY_NO_BINDINGS | Portal enabled, but no interface bindings found in config. Verify via WebUI. |
EXPOSED_LIKELY | Portal enabled and bound to a zone whose name suggests untrusted exposure (untrust, dmz, guest, external, internet, wan, public). Investigate first. |
REVIEW_POLICY | Portal enabled and bound to internal-named zones. Walk security policy to confirm reachability. |
EXPOSED_LIKELY is a hint, not a verdict — zone names are operator conventions, not enforcement. The script cannot replace a security-policy walk; it sorts a fleet output to where attention should land first.
Do:
<captive-portal> blocks per vsys (the schema element name is always captive-portal, regardless of UI naming or PAN-OS version).EXPOSED_LIKELY rows surface first in fleet output.X-PAN-KEY header so it stays out of URL access logs and process arguments.Do not:
The PAN-OS API key inherits the role and permissions of the user who generates it.
Important caveat: there is no read-only XML API grant in PAN-OS.
Admin Role permissions on the XML API tab are binary Enable/Disable per category — Configuration, Operational Requests, Commit, Export, Import, Report, Log, User-ID Agent. Enabling Configuration for XML API permits both
showandset/edit/delete/move/rename/cloneactions. Enabling Operational Requests permits arbitrary<request>operations including<request><restart>. The Web UI and REST API tabs offer Read-Only as an option; the XML API tab does not.An "audit user" created by enabling these XML API categories holds full XML API write authority on the device. Treat the resulting key as a privileged credential: scope its lifetime to the audit window, store it in a secrets manager, and disable the user when the work is complete.
Canonical PAN reference: Get Your API Key (PAN-OS XML API).
On the firewall (or Panorama, if you're using audit_fleet.py):
Create the Admin Role. Navigate to Device > Admin Roles (or Panorama > Admin Roles). Click Add.
audit-config-and-op.Create the audit user. Navigate to Device > Administrators (or Panorama > Administrators). Click Add.
audit-cve-2026-0300.audit-config-and-op.Generate the API key with the new user's credentials. PAN-OS does not expose a "generate API key" button in the WebUI — the keygen endpoint is the supported path. Use a POST body so credentials are not logged in URL access logs:
read -rs AUDIT_PW; echo
export PAN_API_KEY="$(curl -ksX POST "https://fw1.example.com/api/" \
--data-urlencode "type=keygen" \
--data-urlencode "user=audit-cve-2026-0300" \
--data-urlencode "password=$AUDIT_PW" \
| grep -oE '<key>[^<]+' | cut -c6-)"
unset AUDIT_PW
read -rs reads the password without echoing it to the terminal and without putting it in shell history. The script reads the resulting PAN_API_KEY from the environment and sends it via the X-PAN-KEY HTTP header — neither the password nor the key appears in URL access logs or in ps auxww while the script runs.
Disable the user when the audit work is complete. Navigate to Device > Administrators, locate audit-cve-2026-0300, click Delete (or disable the user). Commit. This invalidates any keys generated for that user.
read -rs covers the password. For the key in PAN_API_KEY, prefer setting it interactively via the snippet above rather than pasting from a file. If you must source it from a file, use set +o history for the session.--insecure flag in both audit scripts disables TLS hostname check and certificate validation entirely. Any attacker on path can MITM the audit response and report portal_enabled=False for a rooted device. The audit_fleet.py script requires --accept-mitm-risk alongside --insecure for fleet sweeps because the blast radius of a poisoned response is fleet-wide.0 — no portal enabled on any audited vsys2 — no vsys found (transient or unsupported configuration)3 — portal enabled on at least one vsys (review required)4 — API call failed (auth, TLS, network)The non-zero exit on "portal enabled" is intentional — it lets you wire the script into CI / fleet-sweep pipelines that need a clean signal.
Licensed under the Apache License, Version 2.0. See LICENSE for the full terms.
Commercial use is permitted; attribution is required (retain the copyright notice and license file in derivative works).
Copyright 2026 Tailwind Resource Group.