Security Audit Report
Repository: thomaschristory/netbox-proxy-plugin
Audit date: 2026-06-11
Branch/ref audited: main (HEAD)
Scope & Methodology
A full security audit was performed against the repository via the GitHub API, covering:
- Supply-chain / CVE analysis — dependency review against the GitHub Advisory DB, version-pinning and lockfile inspection.
- Static application security testing (SAST) — pattern-based source review of the plugin's models, templates, serializers, and routing logic.
- Secret scanning — review of the current tree for committed credentials and exposed secrets.
- CI / build / provenance review — analysis of GitHub Actions workflows (
ci.yml, publish.yml), token permissions, action pinning, and the OIDC PyPI publishing pipeline.
Limitations:
- SAST was pattern-based (no full inter-procedural dataflow engine), so some dataflow-dependent issues may not be surfaced.
- The secret scan covered the current HEAD only and did not include full git history.
During verification, 2 candidate findings were investigated and dismissed as false positives. The findings below are the confirmed, adversarially verified results.
Summary of Findings
| Severity |
Count |
| Critical |
0 |
| High |
1 |
| Medium |
1 |
| Low |
3 |
| Info |
7 |
| Total |
12 |
High
H-1 — Proxy password rendered in plaintext on the object detail page
- Severity: High
- Category: Sensitive Data Exposure / Credential Leakage
- Location:
netbox_proxy_plugin/templates/netbox_proxy_plugin/proxy.html:30-31
Evidence:
<th scope="row">Proxy URL</th>
<td><code>{{ object.url }}</code></td>
The Proxy.url property in models.py builds f"{self.protocol}://{self.username}:{self.password}@{self.server}:{self.port}", embedding the plaintext password whenever a username is present.
Impact: Anyone who can view the proxy detail page sees the credential password in cleartext. The secret is present in the rendered page, the HTML source, browser history, and any page caches or screenshots. The same url value is also exposed via the API serializer (it omits the raw password field but exposes the derived url), so the password leaks through API responses as well. Exposure is gated by NetBox authenticated/permissioned object views, which keeps this at High rather than Critical.
Remediation: Render a password-masked form of the URL — show protocol, server, port, and username, but replace the password component with *** (or omit it). Never render object.url containing the raw password in the UI or expose it in the API serializer. Add a masked display property and use it everywhere the URL is surfaced.
Medium
M-1 — Proxy credentials stored in plaintext in the database
- Severity: Medium
- Category: Insecure Storage of Credentials
- Location:
netbox_proxy_plugin/models.py:48-51
Evidence:
password = models.CharField(
max_length=255,
blank=True,
)
There is no field-level encryption: no encrypted field class, no save()/clean() transform, and the migration (migrations/0001_initial.py) persists the column as a plain varchar.
Impact: Proxy credentials are stored unencrypted in PostgreSQL and in every database backup. Anyone with DB read access, a backup, or SQL-level access can recover all proxy passwords. This is the root cause that makes the UI/API leaks fully cleartext. NetBox offers encrypted credential facilities (the Secrets store, DataSource backend params), so a bare CharField is below the platform's own standard for comparable secrets. Exploitation requires database/backup access rather than direct network exposure, hence Medium.
Remediation: Store the password encrypted at rest (integrate with NetBox's secret/encrypted-field facilities or a field-level encryption library), or store only a reference/credential ID resolved from a secrets manager. At minimum, never echo it back via API/UI (see H-1).
Low
L-1 — Third-party action astral-sh/setup-uv pinned to a mutable tag inside the OIDC publishing job
- Severity: Low
- Category: Supply-chain / Action Pinning
- Location:
.github/workflows/publish.yml (publish job)
Evidence:
permissions:
id-token: write
steps:
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Publish to PyPI
run: uv publish dist/*
Impact: The publish job holds id-token: write (OIDC for PyPI Trusted Publishing) and runs the third-party action astral-sh/setup-uv@v4 in the same job immediately before publishing. @v4 is a mutable major tag that the upstream owner — or anyone compromising that repo — can re-point at arbitrary code, which would then execute with access to the OIDC id-token and the build artifacts, enabling token exfiltration or artifact tampering. Mitigated by the job using environment: pypi (deployment protection) and by id-token: write being the minimum required scope, keeping this at Low (defense-in-depth rather than an active exploit path).
Remediation: Pin the action to a full 40-character commit SHA (e.g. astral-sh/setup-uv@<sha> # v4.x). Consider whether uv is even required in the publish job (the build job already produced the artifacts) to minimize third-party code alongside the OIDC token. Add Dependabot/Renovate for the github-actions ecosystem to keep SHAs current.
L-2 — Third-party actions pinned to mutable major tags in CI
- Severity: Low
- Category: Supply-chain / Action Pinning
- Location:
.github/workflows/ci.yml (lint and build jobs)
Evidence:
- name: Install uv
uses: astral-sh/setup-uv@v4
First-party actions (actions/checkout@v4, actions/upload-artifact@v4, actions/download-artifact@v4) are likewise tag-pinned but are lower risk as GitHub-owned.
Impact: Mutable major tags mean the executed action code can change without any commit to this repo, undermining reproducibility and provenance. The CI workflow handles no secrets and has no elevated permissions, which caps real-world impact at Low.
Remediation: Pin third-party actions to full commit SHAs with a version comment; optionally pin first-party actions for consistency. Enable Dependabot for the github-actions ecosystem.
L-3 — GitHub Actions pinned to mutable major-version tags instead of commit SHAs (repository-wide)
- Severity: Low
- Category: CI Supply-chain
- Location:
.github/workflows/ci.yml, .github/workflows/publish.yml
Evidence: uses: actions/checkout@v4, uses: astral-sh/setup-uv@v4, uses: actions/upload-artifact@v4, uses: actions/download-artifact@v4. The publish job carries permissions: id-token: write and runs uv publish dist/*.
Impact: All third-party GitHub Actions are referenced by mutable @v4 tags rather than immutable SHAs across both workflows. Tags can be force-moved by an action maintainer (or an attacker who compromises the action repo) to malicious code that runs in CI — most sensitively in publish.yml, which holds the OIDC publishing permission. astral-sh/setup-uv is the highest trust-sensitivity item as a third-party action bootstrapping the build/publish toolchain. This is a defense-in-depth hardening gap (SLSA / GitHub hardening guidance), not an exploitable flaw, hence Low.
Remediation: Pin all actions to full commit SHAs (e.g. astral-sh/setup-uv@<sha> # v4) and enable Dependabot for the github-actions ecosystem to keep SHAs updated. Prioritize the publish workflow given its OIDC publishing permission.
Informational
I-1 — CI workflow lacks an explicit least-privilege permissions block
- Category: Token Permissions
- Location:
.github/workflows/ci.yml (top-level / job level — absent)
Evidence: ci.yml defines no permissions: block at workflow or job level, so it inherits the repository/organization default GITHUB_TOKEN permissions.
Impact: Relying on the default is implicit and could silently become permissive if the org default is set to read/write. The CI jobs (ruff, uv build) need no write scopes. No concrete exploit path exists; pure defense-in-depth.
Remediation: Add an explicit top-level permissions: contents: read (or {}) to ci.yml so it is least-privilege regardless of the repo/org default.
I-2 — Proxy credentials embedded in URL property (schema-level design observation)
- Category: Secret-storage Design
- Location:
netbox_proxy_plugin/models.py:45-52, 78-83
Evidence: username and password are plain CharField(max_length=255) columns; the url property interpolates them directly into a credentialed URL. No actual credential value is committed to the repo — this is the data model only.
Impact: Operator-entered proxy passwords are persisted in cleartext and surfaced in the detail template and via the derived url field in the API serializer. This is the design-level framing of H-1 and M-1; not a leaked secret.
Remediation: Document that passwords are stored in plaintext; mask the password in any url value used for display; verify the derived url does not leak the password in API responses (it currently does).
I-3 — Spoofable URL-substring proxy routing heuristic
- Category: Weak Routing / Input Validation (SSRF-adjacent)
- Location:
netbox_proxy_plugin/proxy_router.py:57-63
Evidence:
if url:
if "github" in url:
return "release_check"
if "plugin" in url or "catalog" in url:
return "plugin_catalog"
Impact: Naive substring matching can misfire on unrelated hostnames/paths (e.g. github.evil.com, .../catalog/...), causing misrouting among admin-configured trusted proxies. The router only selects among proxies an administrator has already created and tagged and never fetches the URL itself, so this is not SSRF. The heuristic is only reached when no caller context is supplied; primary subsystems use authoritative class-based routing. Worst case is benign misrouting among trusted infrastructure.
Remediation: Replace substring checks with parsed-hostname matching (urllib.parse + exact host / suffix allowlist, e.g. host == 'github.com' or host.endswith('.github.com')). Prefer context['client'] class-based routing as authoritative; treat URL heuristics as a strict-host-match last resort.
I-4 — Dependabot alerts disabled for the repository
- Category: Monitoring
- Location: Repository settings
Evidence: GET repos/thomaschristory/netbox-proxy-plugin/dependabot/alerts → HTTP 403 "Dependabot alerts are disabled for this repository."
Impact: No automated alerting for vulnerable dependencies. Currently low-impact given the near-empty runtime dependency graph, but future CVEs in NetBox/Django/transitive deps or pinned Actions would not be surfaced.
Remediation: Enable Dependabot alerts and security updates for the pip and github-actions ecosystems in Settings → Code security.
I-5 — Dev dependency ruff is unpinned (floating version)
- Category: Version Pinning
- Location:
pyproject.toml — [project.optional-dependencies] dev = ["ruff"]
Evidence: dev = ["ruff"] (no version specifier). GitHub Advisory DB returned zero PIP vulnerabilities for ruff.
Impact: Any release of ruff (including a future breaking/malicious one) is acceptable, and there is no lockfile, so the dev toolchain is non-reproducible. Impact is limited: ruff is a dev-only linter/formatter under optional dependencies, never installed by end users, with no known advisories.
Remediation: Pin a range, e.g. ruff>=0.6,<0.7, and consider a uv.lock for the dev environment (the project already uses uv in CI).
I-6 — No lockfile present (no reproducible/pinned dependency set)
- Category: Reproducibility
- Location: Repository root
Evidence: The tree contains pyproject.toml but no uv.lock, requirements*.txt, or poetry.lock. CI uses astral-sh/setup-uv@v4 + uv build / uvx ruff. (.gitignore explicitly excludes uv.lock.)
Impact: No pinned, hash-verified resolved dependency set. Impact is negligible here: pyproject.toml declares zero runtime dependencies, uv build (hatchling) does not consume a lockfile, and uvx ruff runs in an ephemeral tool environment. This is a reproducibility/hygiene observation, not a supply-chain risk in the current state.
Remediation: Optional for a build-only library. Once real runtime dependencies are declared, commit a uv.lock (uv lock) so build inputs and dev tooling are pinned and hash-verified.
I-7 — Documentation placeholder credentials (admin/admin, <your-token>)
- Category: Placeholder
- Location:
README.md (dev login section and REST API curl examples)
Evidence: # Login: admin / admin and Authorization: Token <your-token>. The dev/ directory is gitignored and absent from HEAD; no real environment or secret is committed.
Impact: None — these are unambiguous documentation placeholders for a local Docker dev environment, not real or leaked credentials.
Remediation: No action required. Optionally note that the admin/admin default is for local development only.
This report reflects findings confirmed at HEAD on branch main as of 2026-06-11. Severities use the adversarially verified adjusted values. SAST was pattern-based and the secret scan covered HEAD only (not git history).
Security Audit Report
Repository:
thomaschristory/netbox-proxy-pluginAudit date: 2026-06-11
Branch/ref audited:
main(HEAD)Scope & Methodology
A full security audit was performed against the repository via the GitHub API, covering:
ci.yml,publish.yml), token permissions, action pinning, and the OIDC PyPI publishing pipeline.Limitations:
During verification, 2 candidate findings were investigated and dismissed as false positives. The findings below are the confirmed, adversarially verified results.
Summary of Findings
High
H-1 — Proxy password rendered in plaintext on the object detail page
netbox_proxy_plugin/templates/netbox_proxy_plugin/proxy.html:30-31Evidence:
The
Proxy.urlproperty inmodels.pybuildsf"{self.protocol}://{self.username}:{self.password}@{self.server}:{self.port}", embedding the plaintext password whenever a username is present.Impact: Anyone who can view the proxy detail page sees the credential password in cleartext. The secret is present in the rendered page, the HTML source, browser history, and any page caches or screenshots. The same
urlvalue is also exposed via the API serializer (it omits the rawpasswordfield but exposes the derivedurl), so the password leaks through API responses as well. Exposure is gated by NetBox authenticated/permissioned object views, which keeps this at High rather than Critical.Remediation: Render a password-masked form of the URL — show protocol, server, port, and username, but replace the password component with
***(or omit it). Never renderobject.urlcontaining the raw password in the UI or expose it in the API serializer. Add a masked display property and use it everywhere the URL is surfaced.Medium
M-1 — Proxy credentials stored in plaintext in the database
netbox_proxy_plugin/models.py:48-51Evidence:
There is no field-level encryption: no encrypted field class, no
save()/clean()transform, and the migration (migrations/0001_initial.py) persists the column as a plainvarchar.Impact: Proxy credentials are stored unencrypted in PostgreSQL and in every database backup. Anyone with DB read access, a backup, or SQL-level access can recover all proxy passwords. This is the root cause that makes the UI/API leaks fully cleartext. NetBox offers encrypted credential facilities (the Secrets store, DataSource backend params), so a bare
CharFieldis below the platform's own standard for comparable secrets. Exploitation requires database/backup access rather than direct network exposure, hence Medium.Remediation: Store the password encrypted at rest (integrate with NetBox's secret/encrypted-field facilities or a field-level encryption library), or store only a reference/credential ID resolved from a secrets manager. At minimum, never echo it back via API/UI (see H-1).
Low
L-1 — Third-party action
astral-sh/setup-uvpinned to a mutable tag inside the OIDC publishing job.github/workflows/publish.yml(publish job)Evidence:
Impact: The publish job holds
id-token: write(OIDC for PyPI Trusted Publishing) and runs the third-party actionastral-sh/setup-uv@v4in the same job immediately before publishing.@v4is a mutable major tag that the upstream owner — or anyone compromising that repo — can re-point at arbitrary code, which would then execute with access to the OIDC id-token and the build artifacts, enabling token exfiltration or artifact tampering. Mitigated by the job usingenvironment: pypi(deployment protection) and byid-token: writebeing the minimum required scope, keeping this at Low (defense-in-depth rather than an active exploit path).Remediation: Pin the action to a full 40-character commit SHA (e.g.
astral-sh/setup-uv@<sha> # v4.x). Consider whetheruvis even required in the publish job (the build job already produced the artifacts) to minimize third-party code alongside the OIDC token. Add Dependabot/Renovate for thegithub-actionsecosystem to keep SHAs current.L-2 — Third-party actions pinned to mutable major tags in CI
.github/workflows/ci.yml(lint and build jobs)Evidence:
First-party actions (
actions/checkout@v4,actions/upload-artifact@v4,actions/download-artifact@v4) are likewise tag-pinned but are lower risk as GitHub-owned.Impact: Mutable major tags mean the executed action code can change without any commit to this repo, undermining reproducibility and provenance. The CI workflow handles no secrets and has no elevated permissions, which caps real-world impact at Low.
Remediation: Pin third-party actions to full commit SHAs with a version comment; optionally pin first-party actions for consistency. Enable Dependabot for the
github-actionsecosystem.L-3 — GitHub Actions pinned to mutable major-version tags instead of commit SHAs (repository-wide)
.github/workflows/ci.yml,.github/workflows/publish.ymlEvidence:
uses: actions/checkout@v4,uses: astral-sh/setup-uv@v4,uses: actions/upload-artifact@v4,uses: actions/download-artifact@v4. Thepublishjob carriespermissions: id-token: writeand runsuv publish dist/*.Impact: All third-party GitHub Actions are referenced by mutable
@v4tags rather than immutable SHAs across both workflows. Tags can be force-moved by an action maintainer (or an attacker who compromises the action repo) to malicious code that runs in CI — most sensitively inpublish.yml, which holds the OIDC publishing permission.astral-sh/setup-uvis the highest trust-sensitivity item as a third-party action bootstrapping the build/publish toolchain. This is a defense-in-depth hardening gap (SLSA / GitHub hardening guidance), not an exploitable flaw, hence Low.Remediation: Pin all actions to full commit SHAs (e.g.
astral-sh/setup-uv@<sha> # v4) and enable Dependabot for thegithub-actionsecosystem to keep SHAs updated. Prioritize the publish workflow given its OIDC publishing permission.Informational
I-1 — CI workflow lacks an explicit least-privilege permissions block
.github/workflows/ci.yml(top-level / job level — absent)Evidence:
ci.ymldefines nopermissions:block at workflow or job level, so it inherits the repository/organization defaultGITHUB_TOKENpermissions.Impact: Relying on the default is implicit and could silently become permissive if the org default is set to read/write. The CI jobs (ruff,
uv build) need no write scopes. No concrete exploit path exists; pure defense-in-depth.Remediation: Add an explicit top-level
permissions: contents: read(or{}) toci.ymlso it is least-privilege regardless of the repo/org default.I-2 — Proxy credentials embedded in URL property (schema-level design observation)
netbox_proxy_plugin/models.py:45-52, 78-83Evidence:
usernameandpasswordare plainCharField(max_length=255)columns; theurlproperty interpolates them directly into a credentialed URL. No actual credential value is committed to the repo — this is the data model only.Impact: Operator-entered proxy passwords are persisted in cleartext and surfaced in the detail template and via the derived
urlfield in the API serializer. This is the design-level framing of H-1 and M-1; not a leaked secret.Remediation: Document that passwords are stored in plaintext; mask the password in any
urlvalue used for display; verify the derivedurldoes not leak the password in API responses (it currently does).I-3 — Spoofable URL-substring proxy routing heuristic
netbox_proxy_plugin/proxy_router.py:57-63Evidence:
Impact: Naive substring matching can misfire on unrelated hostnames/paths (e.g.
github.evil.com,.../catalog/...), causing misrouting among admin-configured trusted proxies. The router only selects among proxies an administrator has already created and tagged and never fetches the URL itself, so this is not SSRF. The heuristic is only reached when no caller context is supplied; primary subsystems use authoritative class-based routing. Worst case is benign misrouting among trusted infrastructure.Remediation: Replace substring checks with parsed-hostname matching (
urllib.parse+ exact host / suffix allowlist, e.g.host == 'github.com'orhost.endswith('.github.com')). Prefercontext['client']class-based routing as authoritative; treat URL heuristics as a strict-host-match last resort.I-4 — Dependabot alerts disabled for the repository
Evidence:
GET repos/thomaschristory/netbox-proxy-plugin/dependabot/alerts→ HTTP 403 "Dependabot alerts are disabled for this repository."Impact: No automated alerting for vulnerable dependencies. Currently low-impact given the near-empty runtime dependency graph, but future CVEs in NetBox/Django/transitive deps or pinned Actions would not be surfaced.
Remediation: Enable Dependabot alerts and security updates for the
pipandgithub-actionsecosystems in Settings → Code security.I-5 — Dev dependency
ruffis unpinned (floating version)pyproject.toml—[project.optional-dependencies] dev = ["ruff"]Evidence:
dev = ["ruff"](no version specifier). GitHub Advisory DB returned zero PIP vulnerabilities forruff.Impact: Any release of
ruff(including a future breaking/malicious one) is acceptable, and there is no lockfile, so the dev toolchain is non-reproducible. Impact is limited:ruffis a dev-only linter/formatter under optional dependencies, never installed by end users, with no known advisories.Remediation: Pin a range, e.g.
ruff>=0.6,<0.7, and consider auv.lockfor the dev environment (the project already usesuvin CI).I-6 — No lockfile present (no reproducible/pinned dependency set)
Evidence: The tree contains
pyproject.tomlbut nouv.lock,requirements*.txt, orpoetry.lock. CI usesastral-sh/setup-uv@v4+uv build/uvx ruff. (.gitignoreexplicitly excludesuv.lock.)Impact: No pinned, hash-verified resolved dependency set. Impact is negligible here:
pyproject.tomldeclares zero runtime dependencies,uv build(hatchling) does not consume a lockfile, anduvx ruffruns in an ephemeral tool environment. This is a reproducibility/hygiene observation, not a supply-chain risk in the current state.Remediation: Optional for a build-only library. Once real runtime dependencies are declared, commit a
uv.lock(uv lock) so build inputs and dev tooling are pinned and hash-verified.I-7 — Documentation placeholder credentials (
admin/admin,<your-token>)README.md(dev login section and REST API curl examples)Evidence:
# Login: admin / adminandAuthorization: Token <your-token>. Thedev/directory is gitignored and absent from HEAD; no real environment or secret is committed.Impact: None — these are unambiguous documentation placeholders for a local Docker dev environment, not real or leaked credentials.
Remediation: No action required. Optionally note that the
admin/admindefault is for local development only.This report reflects findings confirmed at HEAD on branch
mainas of 2026-06-11. Severities use the adversarially verified adjusted values. SAST was pattern-based and the secret scan covered HEAD only (not git history).