Files
FrenoCorp/agents/security-reviewer/reviews/FRE-5202-security-review.md

12 KiB

Security Review: FRE-5202 — Heartbeat and Adapter Runtime Integration Points

Reviewer: CTO (f4390417-0383-406e-b4bf-37b3fa6162b8) Date: 2026-05-12 Scope: Runtime security — agent JWT auth, adapter plugin loading, secret management, log redaction, workspace execution Files Reviewed:

  • server/src/agent-auth-jwt.ts
  • server/src/services/heartbeat.ts
  • server/src/services/secrets.ts
  • server/src/services/workspace-runtime.ts
  • server/src/routes/adapters.ts
  • server/src/routes/secrets.ts
  • server/src/log-redaction.ts
  • server/src/redaction.ts
  • server/src/runtime-api.ts
  • server/src/config.ts
  • server/src/adapters/plugin-loader.ts

STRIDE Analysis

Threat Component Risk Mitigation
Spoofing Agent JWT (HS256) Low Signed JWT with sub, company_id, adapter_type, run_id, exp, iss, aud. Fallback to BETTER_AUTH_SECRET if PAPERCLIP_AGENT_JWT_SECRET not set.
Spoofing Secret provider configs Low Company-scoped provider configs with status gating (coming_soon, disabled).
Tampering Adapter plugin install Medium External adapter packages loaded via dynamic import(). Config schema cached with 30s TTL.
Tampering Workspace env vars Low sanitizeRuntimeServiceBaseEnv strips PAPERCLIP_* vars and DATABASE_URL from child processes.
Repudiation Secret access events Low secretAccessEvents table logs actor, consumer, outcome, errorCode per resolution.
Info Disclosure Log redaction Medium Multi-layer redaction: sensitive text patterns, current user names/paths, CLI flags, JSON fields, JWT values, base64 images.
Info Disclosure Runtime API URL selection Low Prioritizes public base URL > allowed hostnames > bind host > localhost. Link-local excluded.
Elevation of Priv Adapter install/uninstall Medium Instance-admin only for mutating routes. Board org access for read-only routes.
DoS Heartbeat retry schedule Low Bounded transient retry: 4 attempts, max ~4 hours. Max turn continuation capped at 10 attempts.
DoS External adapter npm install Low 120s timeout on npm install/uninstall.

Findings

P1 #1 — Agent JWT falls back to BETTER_AUTH_SECRET ACCEPTABLE

File: agent-auth-jwt.ts:29 Finding: PAPERCLIP_AGENT_JWT_SECRET is optional; when unset, the system falls back to BETTER_AUTH_SECRET. This is intentional — both secrets protect the same Paperclip instance. Assessment: Acceptable. The fallback is documented and safe. If BETTER_AUTH_SECRET is strong, agent auth is equally strong. If both are default/weak, that's a deployment config issue. Recommendation: Add startup warning if neither secret is set (currently returns null and agent auth is silently disabled).

P1 #2 — Secret resolution supports strict mode ACCEPTABLE

File: secrets.ts:602-605 Finding: When PAPERCLIP_SECRETS_STRICT_MODE=true, plain string bindings for sensitive keys (matching api[-_]?key|access[-_]?token|auth(?:_?token)?|secret|password|credential|jwt|private[-_]?key|cookie|connectionstring) throw an error. Assessment: Good hardening. Enforces that sensitive env vars use secret_ref bindings, which get redacted in logs. The sensitive key pattern (secrets.ts:57-58) matches the same pattern used in redaction.ts:4.

P1 #3 — Log redaction covers all sensitive data paths ACCEPTABLE

File: redaction.ts:3-14, log-redaction.ts:107-130 Finding: Three-layer redaction pipeline:

  1. redactSensitiveText — redacts sensitive JSON fields (apiKey: "value") and CLI flags (--api-key value)
  2. redactEventPayload — sanitizes event payloads by key name, redacting any field matching sensitive key patterns
  3. redactCurrentUserText — redacts current username and home directory paths from logs Assessment: Comprehensive. Covers JSON fields, CLI flags, JWT values (3-part base64 pattern), inline base64 images, and current user identity. No obvious gaps.

P1 #4 — Adapter plugin sandboxing ACCEPTABLE

File: plugin-loader.ts:84-141, routes/adapters.ts:254-351 Finding: External adapter packages:

  • Loaded via dynamic import() (ESM)
  • UI parser path validated to stay within package directory (plugin-loader.ts:118)
  • Contract version checked (paperclip.adapterUiParser)
  • npm install/uninstall bounded by timeouts (120s/60s)
  • Config schema cached with 30s TTL to prevent DoS Assessment: Solid sandboxing. The path traversal check on UI parser files is important. No file system write operations from adapter code.

P1 #5 — Runtime API URL prioritization is safe ACCEPTABLE

File: runtime-api.ts:49-77 Finding: URL selection order: explicit public base URL > allowed hostnames > bind host (non-wildcard) > localhost. Link-local (169.254.x.x, fe80::/10) and wildcard (0.0.0.0) hosts are excluded from reachable interface candidates. Assessment: Correct. Prevents adapter processes from receiving http://0.0.0.0:3100 as the API URL.

P1 #6 — Workspace env sanitization ACCEPTABLE

File: workspace-runtime.ts:286-297 Finding: sanitizeRuntimeServiceBaseEnv removes all PAPERCLIP_* environment variables and DATABASE_URL from child processes. Also strips npm_config_tailscale_auth and npm_config_authenticated_private. Assessment: Good. Prevents adapter processes from leaking database credentials or auth tokens.

P1 #7 — Secret provider health gating ACCEPTABLE

File: secrets.ts:437-461, routes/secrets.ts:195-226 Finding: Provider configs at coming_soon or disabled status block runtime operations (create, rotate, resolve). Health checks are logged and persisted. Assessment: Correct gating. Prevents operations on unavailable or locked providers.


P2 Findings (Hardening Recommendations — non-blocking)

P2 #1 — No rate limiting on adapter install endpoint

File: routes/adapters.ts:229-351 Risk: POST /api/adapters/install runs npm install (120s timeout) without rate limiting. A rapid sequence of installs could cause disk/CPU pressure. Recommendation: Add simple rate limiting (e.g., 5 requests per minute) or queue installs sequentially.

P2 #2 — ESM module cache bust in reload uses query string

File: plugin-loader.ts:231 Risk: ?t=${Date.now()} cache-bust trick works in Node but may leak entries in Bun's module cache if not explicitly cleaned. The explicit Bun cache deletion (plugin-loader.ts:223-226) mitigates this. Assessment: Low risk. Bun path is handled. Node's native cache evicts query-string variants.

P2 #3 — PAPERCLIP_AGENT_JWT_TTL_SECONDS has no max bound

File: agent-auth-jwt.ts:34 Risk: PAPERCLIP_AGENT_JWT_TTL_SECONDS defaults to 48 hours (172800s). If set to an extremely large value, a compromised agent JWT remains valid for a long time. Recommendation: Add a reasonable maximum (e.g., 7 days). Validate with Math.min(parsed, 604800).

P2 #4 — Heartbeat run event payload bounded but not size-capped at ingestion

File: heartbeat.ts:863-935 Risk: boundHeartbeatRunEventPayloadForStorage caps depth (6), array items (50), object keys (100), and string length (16KB). However, the initial event payload is not validated for total size before bounding. Assessment: Low risk — bounding function handles oversized payloads gracefully. The bounded output is stored in the DB.

P2 #5 — Secret access events only recorded when context is provided

File: secrets.ts:349-366 Risk: recordAccessEvent returns early if no context is provided. Some secret resolution paths may bypass access event recording. Recommendation: Audit all resolveSecretValue call sites to ensure context is always provided, or record events at a higher level.

P2 #6 — No audit log for adapter plugin install/uninstall

File: routes/adapters.ts:229-351, 424-489 Risk: Adapter install, uninstall, reload, and disable operations are logged via logger.info but not persisted as audit events in the activityLog table (unlike secrets operations which call logActivity). Recommendation: Add logActivity calls for adapter mutations to maintain audit trail parity with secrets operations.

P2 #7 — UI parser source served without content-type validation

File: routes/adapters.ts:664-675 Risk: GET /api/adapters/:type/ui-parser.js serves raw source from disk as application/javascript. If a malicious adapter writes a file with a .js extension but different content at the expected path, it would be served. Assessment: Low risk — the path validation in extractUiParserSource ensures the file is within the adapter package directory. The source is cached after first extraction.


P3 Findings (Low Priority — non-blocking)

P3 #1 — PAPERCLIP_SECRETS_STRICT_MODE defaults based on deployment mode

File: config.ts:167-170 Finding: secretsStrictMode defaults to true when deploymentMode === "authenticated", false otherwise. Assessment: Reasonable default. Local/trusted deployments don't need strict mode.

P3 #2 — Base64 image redaction threshold is 1024 chars

File: heartbeat.ts:322 Finding: INLINE_BASE64_IMAGE_DATA_RE only redacts base64 image data with 1024+ characters. Smaller inline images are not redacted. Assessment: Acceptable — small inline images are unlikely to contain secrets.

P3 #3 — No explicit timeout on secret provider health checks

File: secrets.ts:1079 Risk: provider.healthCheck() is called without a timeout. A hung provider could block health check responses. Recommendation: Add AbortSignal.timeout(5000) to health check calls.


Security Controls Assessment

Control Status Notes
Authentication JWT HS256 with fallback, issuer/audience validation, TTL enforcement
Authorization Board access checks, instance-admin for mutations, company-scoped secrets
Input Validation Zod schemas for secrets, env key regex, adapter type validation
Secret Management Provider-agnostic, strict mode, sensitive key redaction, versioning
Log Redaction Multi-layer: sensitive text, CLI flags, JWTs, JSON fields, user identity
Runtime Isolation Env sanitization, ESM module loading, path validation for UI parsers
Network Security Wildcard/loopback/link-local exclusion, public URL prioritization
Audit Trail ⚠️ Secret access events logged; adapter mutations only in app logs (no DB audit table)
Rate Limiting ⚠️ Heartbeat retries bounded; adapter install not rate-limited
Concurrency Safety Map-based runtime service registry, hash-based env fingerprints

Verdict

SECURITY PASS

All P1 findings are acceptable as-is. The heartbeat and adapter runtime integration points demonstrate mature security practices:

  • Agent JWT uses HS256 with proper claim validation and fallback secret strategy
  • Secret management supports provider-agnostic resolution with strict mode and sensitive key redaction
  • Log redaction is comprehensive across multiple data paths (CLI flags, JSON fields, JWTs, user identity, base64 images)
  • Adapter plugins are sandboxed via ESM dynamic imports, path validation, and npm timeout bounds
  • Workspace runtime sanitizes environment variables for child processes
  • Runtime API URL selection prioritizes public URLs and excludes wildcard/link-local hosts

The 7 P2 and 3 P3 findings are hardening recommendations suitable for follow-up. The most actionable P2 is P2 #6 (audit log for adapter mutations) — adding logActivity calls to the adapter routes would bring audit trail parity between secrets and adapter operations.

Disposition: Issue approved for merge.