Skip to content
Open

150 #58

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
114 changes: 114 additions & 0 deletions .kiro/security-assessment-2026-07-02.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Pabawi Security Assessment — 2026-07-02

**Scope:** Backend (`backend/src`) auth/RBAC, command execution, web layer, MCP; frontend token handling and output rendering; dependency and configuration review. Static source review only — no runtime/DAST testing was performed.

**Overall:** The codebase shows a strong baseline: parameterized SQL throughout, bcrypt (cost 12), JWT with `iss`/`aud` pinning and refresh-token rotation + reuse detection, helmet CSP, prototype-pollution guards, constant-time secret comparison, and `spawn(..., { shell: false })` with argv separation. The most serious issue is an **authorization + command-whitelist bypass on the batch/re-execute paths**, which lets any authenticated user run arbitrary commands on target nodes. That should be fixed before anything else.

---

## Findings

### H-1 (High) — Command-whitelist and RBAC bypass via `/api/executions/batch` and `/api/executions/:id/re-execute`

**Location:** `backend/src/server.ts` (executions mount ~L860), `backend/src/routes/executions.ts` (`/batch` L1559, `/:id/re-execute` L755), `backend/src/services/BatchExecutionService.ts` (`executeAction` L620), `backend/src/integrations/bolt/BoltPlugin.ts` (`case "command"` L224), `backend/src/integrations/bolt/BoltService.ts` (`runCommand` L749).

The single-node route `POST /api/nodes/:id/command` is properly gated by `rbacMiddleware('bolt','execute')` **and** `BoltCommandWhitelistService.validateCommand()`, which blocks shell metacharacters (`; | & ` `$() {} * ? [] ~ < > \` newlines) even in `allowAll` mode.

The `/api/executions` router is mounted with `authMiddleware` + `rateLimitMiddleware` only — **no `rbacMiddleware`**. Its `POST /batch` handler accepts `{ type: "command", action: <arbitrary string>, tool: "bolt"|"ansible"|"ssh" }` and runs it through `IntegrationManager.executeAction → BoltPlugin (case "command") → BoltService.runCommand`. That path performs **no whitelist validation** and only rejects a leading `-` (`assertNoLeadingDash`). `re-execute` has the same gap.

Consequences:
- Any authenticated user — regardless of role, including a read-only user — can execute commands on managed nodes. The `bolt:execute` permission is not enforced.
- Because Bolt runs the command in a shell **on the remote target**, shell metacharacters are interpreted remotely. A payload like `whoami; curl … | sh` submitted via `/api/executions/batch` bypasses the metacharacter filter that guards the single-node route. This is effectively remote command injection on managed infrastructure.

**Remediation:**
- Mount `/api/executions` behind `rbacMiddleware('bolt','execute')` (and per-tool checks where appropriate), matching the single-node route.
- Route all command execution through a single choke point that calls `validateCommand()` before spawning — do not let `BatchExecutionService`/`executeAction` reach `runCommand` without whitelist validation.
- Apply the same `SHELL_META_PATTERN` check inside `BoltService.runCommand` as defense-in-depth so no future callsite can bypass it.

---

### H-2 (High) — Vulnerable transitive dependency: `undici`

**Location:** `backend` dependency tree (`npm audit`).

`npm audit --omit=dev` reports one high-severity advisory chain in `undici <=6.26.0` (HTTP header injection via Set-Cookie percent-decoding, WebSocket DoS via fragment-count bypass, response-queue poisoning via keep-alive socket reuse, SameSite downgrade). `npm audit fix` reports a fix is available.

**Remediation:** Run `npm audit fix` (or bump the dependency pulling in `undici`), re-run tests, and add `npm audit` to CI so regressions are caught. Given the CLAUDE.md note that extension npm deps are bundled and not lockfile-tracked, verify any bundled copies of `undici` are also updated.

---

### M-1 (Medium) — Console WebSocket proxy disables upstream TLS verification

**Location:** `backend/src/services/ConsoleWebSocketProxy.ts` L250 — `new WebSocket(upstreamUrl, { rejectUnauthorized: false })`.

The upstream console connection is established with certificate verification disabled unconditionally, exposing the proxied console session (potentially carrying credentials/keystrokes) to MITM on the path to the upstream host.

**Remediation:** Make TLS verification the default and gate any relaxation behind an explicit, per-integration opt-in config flag (as Proxmox/PuppetDB already do), ideally with a startup warning when disabled.

---

### M-2 (Medium) — JWT access and refresh tokens stored in `localStorage`

**Location:** `frontend/src/lib/auth.svelte.ts` (L308–312, L326–330).

Access token, refresh token, and user object are persisted in `localStorage`, which is readable by any JavaScript running in the origin. Combined with the `{@html}` rendering paths (see L-1), any XSS becomes full account/session takeover, including a 7-day refresh token.

**Remediation:** Prefer `HttpOnly`, `Secure`, `SameSite` cookies for token storage so tokens are not reachable from JS. If localStorage must stay, keep the strict CSP (already present) and treat any `{@html}` sink as high-risk.

---

### M-3 (Medium) — "Revoke all user tokens" uses second-granularity `iat` comparison

**Location:** `backend/src/services/AuthenticationService.ts` — `isTokenRevoked` (L600–605), `revokeAllUserTokens` (L537).

Bulk revocation compares `decoded.iat * 1000` (JWT `iat` is second-granularity) against the revocation timestamp in milliseconds. A token minted in the same wall-clock second as the revocation can have `iat*1000 < revokedAt` fail to hold, so it may remain valid despite a "revoke all" being issued. The window is small but real for concurrent logout/revoke-then-reissue flows.

**Remediation:** Store a per-user "tokens invalid before" epoch and reject tokens with `iat <= that epoch` (inclusive), or track individual `jti`s. Avoid relying on sub-second precision from `iat`.

---

### L-1 (Low / Informational) — `{@html}` output rendering (reviewed, currently mitigated)

**Location:** `frontend/src/lib/ansiToHtml.ts` (`ansiToHtml` L94), consumers in `CommandOutput.svelte`, `RealtimeOutputViewer.svelte`, `PuppetOutputViewer.svelte`, `ExecutionsPage.svelte`.

Command/Puppet output is rendered with `{@html}`. This is currently safe: `ansiToHtml` HTML-escapes `& < > " '` **before** injecting `<span>` tags, colors come from a fixed lookup table (no user-controlled style values), and the search-highlight path uses `escapeHtml(stripAnsi(...))`. No injection was found. Flagging because it is a fragile, high-impact sink: any future edit that reorders escaping or adds a user-controlled attribute reintroduces stored XSS over attacker-influenced node output.

**Remediation:** Add a regression test asserting `ansiToHtml('<img src=x onerror=alert(1)>')` produces no live markup, and keep the escape-first ordering invariant documented at the callsites.

---

### L-2 (Low) — Stale documentation advertising deprecated `?token=` JWT-in-URL auth

**Location:** `backend/src/routes/streaming.ts` (docstring L133–134, and `/:id/stream` doc comment).

The implementation correctly resolves auth via single-use `?ticket=` in `streamAuthMiddleware` (JWT no longer accepted in the URL), but the docstrings still describe a `?token=<JWT>` fallback. Misleading docs can lead an operator or future contributor to reintroduce JWT-in-URL (which leaks tokens into access logs).

**Remediation:** Remove the `?token=` references from the docstrings to match the code.

---

### L-3 (Low) — Authenticated config endpoint echoes the command whitelist

**Location:** `backend/src/server.ts` L683 (`GET /api/config`); rate limiting skips `/api/config` in `securityMiddleware.ts` L69–72.

`GET /api/config` returns `commandWhitelist` (allowAll, matchMode, entries) and timeouts. It requires authentication, so exposure is limited, but it does hand any logged-in user the exact allow/deny policy, and it is exempt from the per-user rate limiter. Low impact; note in the context of H-1 (a user who can read the whitelist can also currently bypass it).

**Remediation:** Consider returning whitelist details only to admins, and confirm the rate-limit skip is intended.

---

## Positives worth preserving

- All reviewed SQL uses parameterized queries (`?` placeholders); interpolated fragments are fixed internal column-name constants, not user input.
- Passwords hashed with bcrypt cost 12 in production; strong password policy at setup; generic "Invalid credentials" to prevent enumeration; temporary account lockout (5 attempts / 15 min).
- JWT signed HS256 with pinned `iss`/`aud`; refresh-token rotation with reuse detection that revokes the whole token family; fail-secure revocation checks.
- Production refuses to start without a real `JWT_SECRET` (≥32 chars, placeholder strings rejected).
- Bolt spawned with `shell: false` and argv arrays; single-node command route enforces both RBAC and a metacharacter-blocking whitelist.
- `helmet` CSP (`default-src 'self'`, `object-src 'none'`, `frame-src 'none'`), prototype-pollution guard and depth limit in input sanitization, constant-time token comparison (`tokensEqual`), MCP tools gated by RBAC, OAuth flow uses state + nonce + PKCE with one-time state entries.

## Suggested priority

1. H-1 — close the batch/re-execute RBAC + whitelist bypass.
2. H-2 — `npm audit fix` and add audit to CI.
3. M-1, M-2, M-3 — TLS verification default, cookie-based token storage, revocation epoch precision.
4. L-1..L-3 — regression test for the HTML sink, doc cleanup, config exposure.
1 change: 1 addition & 0 deletions .kiro/specs/azure-entra-id-auth/.config.kiro
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"specId": "2cd475d9-ff37-4ae1-af19-3e546cc2ffff", "workflowType": "requirements-first", "specType": "feature"}
Loading
Loading