This document describes the security architecture and safety measures in Nebo.
Nebo runs an AI agent with access to your computer's file system, shell, and network. Security is enforced at multiple independent layers so that no single bypass can compromise your system. The design principle is defense in depth — every layer assumes the one above it might fail.
┌──────────────────────────────────────────────────────┐
│ Hard Safeguard (unconditional, no bypass possible) │ ← System paths, sudo, disk formatting
├──────────────────────────────────────────────────────┤
│ Origin-Based Restrictions (per request source) │ ← Infrastructure ready, deny list pending
├──────────────────────────────────────────────────────┤
│ Tool Policy & Approval (user-configurable) │ ← Allowlist, approval prompts
├──────────────────────────────────────────────────────┤
│ Capability Permissions (per-category toggles) │ ← File, shell, web, etc.
├──────────────────────────────────────────────────────┤
│ App Sandbox (for third-party apps) │ ← Env isolation, signing, permissions
├──────────────────────────────────────────────────────┤
│ Compiled-Only Binary Policy (anti-self-modification)│ ← No interpreted languages, opaque binaries
├──────────────────────────────────────────────────────┤
│ Network Security (auth, CSRF, headers, rate limits) │ ← JWT, CORS, CSP, rate limiting
└──────────────────────────────────────────────────────┘
File: internal/agent/tools/safeguard.go
The safeguard is an unconditional safety rail that runs inside registry.Execute() before every tool call. It cannot be bypassed by autonomous mode, policy level, user approval, or any setting.
- sudo — Blocked in all forms: direct, piped, chained, subshell. Nebo must never run commands with elevated privileges.
- su — Blocked entirely. Nebo must never switch to another user.
- mkfs (all variants) — Cannot format filesystems
- fdisk / gdisk / cfdisk / sfdisk / sgdisk — Cannot modify partition tables
- parted — Cannot modify disk partitions
- wipefs — Cannot wipe filesystem signatures
- diskutil eraseDisk / eraseVolume / partitionDisk (macOS) — Cannot erase or partition disks
- dd to block devices (
of=/dev/...) — Cannot write raw data to drives
- rm -rf / (all variants including
--no-preserve-root) — Cannot delete root filesystem - Writing to /dev/ — Blocked except
/dev/null,/dev/stdout,/dev/stderr - Fork bombs — Detected and blocked
All read operations are allowed. Only write/edit operations are blocked.
| macOS | Linux | Windows |
|---|---|---|
/System/ |
/bin/, /sbin/ |
C:\Windows\ |
/bin/, /sbin/ |
/usr/bin/, /usr/sbin/, /usr/lib/ |
C:\Program Files\ |
/usr/bin/, /usr/sbin/, /usr/lib/ |
/boot/ |
C:\Program Files (x86)\ |
/etc/ |
/etc/ |
C:\ProgramData\ |
/Library/LaunchDaemons/ |
/proc/, /sys/, /dev/ |
C:\Recovery\ |
/Library/LaunchAgents/ |
/root/ |
|
/private/var/db/ |
/var/lib/dpkg/, /var/lib/rpm/ |
Nebo cannot modify or delete its own database, WAL, or data files. This prevents catastrophic self-harm where the agent destroys its own persistence.
| Platform | Protected Path |
|---|---|
| macOS | ~/Library/Application Support/Nebo/data/ |
| Linux | ~/.config/nebo/data/ |
| Windows | %APPDATA%\Nebo\data\ |
| Override | $NEBO_DATA_DIR/data/ |
This includes nebo.db, nebo.db-wal, nebo.db-shm, and any other files in the data directory.
~/.ssh/— SSH keys and configuration~/.gnupg/— GPG keys and configuration~/.aws/credentials— AWS credentials~/.kube/config— Kubernetes credentials~/.docker/config.json— Docker registry credentials
When rm, chmod, or chown commands target any protected system directory, they are blocked unconditionally.
The safeguard error messages tell the user to perform the operation manually in a terminal. Nebo is not designed for system administration — it helps with development and productivity tasks within user-space directories.
File: internal/agent/tools/origin.go, internal/agent/tools/policy.go
Every request is tagged with an origin that tracks where it came from. The infrastructure for per-origin restrictions is fully implemented — WithOrigin(ctx, origin) / GetOrigin(ctx) propagate origin types through the entire execution chain, and Policy.IsDeniedForOrigin() is checked in the registry before any approval logic.
| Origin | Source | Intended Restrictions |
|---|---|---|
user |
Direct interaction (web UI, CLI) | None (governed by policy) |
system |
Internal tasks (heartbeat, cron, recovery) | None |
comm |
Inter-agent communication | Shell denied |
app |
External app binary | Shell denied |
skill |
Matched skill template | Shell denied |
Current status: The origin deny list (
defaultOriginDenyList()inpolicy.go) returnsnil— restrictions are not yet enforced. The infrastructure is wired and tested, but the deny rules are disabled pending a review of which specific tool actions each origin should be denied. Re-enabling is a one-function change. Until then, the safeguard layer (Section 1) and tool policy layer (Section 3) remain the active defense for non-user origins.
File: internal/agent/tools/policy.go
- Allowlist (default) — Only whitelisted commands auto-approve; everything else prompts
- Deny — All operations require approval
- Full — All operations auto-approve (dangerous, opt-in only)
- On-miss (default) — Prompt when command is not in the allowlist
- Always — Prompt for every operation
- Off — Never prompt
Read-only and inspection commands: ls, pwd, cat, head, tail, grep, find, which, jq, cut, sort, uniq, wc, echo, date, env, git status, git log, git diff, git branch, git show, go version, node --version, python --version
The IsDangerous() function flags: rm -rf, rm -r, sudo, su, chmod 777, chown, dd, mkfs, curl | sh, wget | sh, eval, exec, fork bombs.
When enabled, bypasses all approval prompts. Requires:
- Accepting terms of service with explicit warnings
- Typing "ENABLE" to confirm
- Acknowledging liability
When disabled, all permissions reset to defaults (only Chat & Memory enabled).
Multiple concurrent tool approval requests (from different lanes) queue up instead of overwriting each other. The user sees them one at a time, in order.
Users can enable/disable entire capability categories:
| Category | What It Controls |
|---|---|
| Chat & Memory | Conversations, memory, scheduled tasks (always on) |
| File System | Read, write, edit, search files |
| Shell & Terminal | Execute commands, manage processes |
| Web Browsing | Fetch pages, search, browser automation |
| Contacts & Calendar | Contacts, calendar, reminders, mail |
| Desktop Control | Window management, accessibility, clipboard |
| Media & Capture | Screenshots, image analysis, music, TTS |
| System | Spotlight, keychain, Siri shortcuts, notifications |
Disabled categories prevent the tool from being registered with the agent entirely — the LLM never sees it as an available tool.
Third-party apps (.napp packages) run in a sandboxed environment.
- Allowlist-only environment: Only
PATH,HOME,TMPDIR,LANG,LC_ALL,TZ, andNEBO_APP_*variables are passed to apps - All parent environment variables (API keys, secrets, tokens) are stripped
- Per-app isolated log files with
0600permissions
- Rejects symlinks (path traversal prevention)
- Rejects non-regular files (devices, pipes)
- Checks executable bit
- Enforces size limit (500MB default)
- ED25519 signature verification over raw bytes
- Manifest signature: verifies manifest.json integrity
- Binary signature: SHA256 integrity check + ED25519 verification
- Key ID rotation detection (
SHA256(publicKey)[:8]) - Signing key cache (24-hour TTL, force-refresh on failure)
- Revocation checking (1-hour cache TTL)
Seven-layer defense:
- Symlink rejection
- Hard link rejection
- Path traversal detection (
..and absolute paths blocked) - Path escape check (target must stay within destination directory)
- Filename allowlist (only
manifest.json,binary,signatures.json,ui/*) - Size limits (binary: 500MB, UI files: 5MB, metadata: 1MB)
- Required file validation (all three core files must be present)
Apps must declare every permission they need. Unknown permissions are rejected. Categories include:
network:*(DNS, HTTP, WebSocket)filesystem:read,filesystem:writeshell:execute,shell:backgroundmemory:read,memory:writesession:*,tool:*,model:*,channel:*,mcp:*
Permission changes on app updates require user re-approval.
Status: ENFORCED | Files: internal/apps/sandbox.go, internal/apps/napp.go
Nebo's app platform exclusively runs compiled native binaries. Apps written in interpreted or scripted languages — Node.js, Python, Ruby, PHP, Perl, shell scripts, Java/JVM, .NET/Mono — are rejected. This is a non-negotiable security boundary.
Nebo is an AI agent platform. The agent has file system access — it can read and write files as part of its normal operation. This creates a unique and critical threat that traditional app platforms do not face:
If an app's source code is accessible at runtime, the agent can modify it.
An interpreted app (e.g., a Node.js app with index.js in its directory) exposes its entire logic as readable, modifiable plaintext. The agent — or an attacker who compromises the agent via prompt injection — could:
- Rewrite the app's logic — Change what the app does without the user's knowledge
- Inject backdoors — Add data exfiltration, credential theft, or remote access code
- Bypass permission checks — Remove the app's own security validations
- Escalate privileges — Modify the app to request capabilities it wasn't granted
- Persist across restarts — Modified source code survives process restarts since the changes are on disk
This is not a theoretical risk. It is a direct consequence of combining AI file system access with interpreted code execution.
A compiled native binary (ELF on Linux, Mach-O on macOS, PE on Windows) is opaque to the agent:
- The agent cannot meaningfully read or understand compiled machine code
- Random byte modifications to a compiled binary will crash it, not change its behavior
- The binary's logic is not accessible as modifiable source text
- ED25519 signature verification (see Section 5) detects any tampering
- The binary is a sealed, verified artifact — what was signed is what runs
Interpreted App (REJECTED):
┌──────────────────────────────┐
│ app/ │
│ ├── index.js ← Agent can read, modify, inject code
│ ├── package.json │
│ └── node_modules/ │
│ └── ... ← Thousands of modifiable JS files
└──────────────────────────────┘
Compiled App (REQUIRED):
┌──────────────────────────────┐
│ app/ │
│ ├── binary ← Opaque machine code, signed, verified
│ ├── manifest.json │
│ └── signatures.json │
└──────────────────────────────┘
Any language that compiles to a native binary with no runtime interpreter dependency:
| Language | Status | Notes |
|---|---|---|
| Go | Recommended | Static binary, cross-compiles easily, proto/gRPC ecosystem |
| Rust | Supported | Static binary, excellent security properties |
| C / C++ | Supported | Native binary, requires careful memory management |
| Zig | Supported | Native binary, C-interop, cross-compilation |
Any language that requires an interpreter, VM, or runtime to execute:
| Language | Status | Reason |
|---|---|---|
| Node.js / JavaScript | Rejected | Source code is plaintext .js files — fully readable and modifiable |
| Python | Rejected | Source code is plaintext .py files; .pyc bytecode is trivially decompilable |
| Ruby | Rejected | Source code is plaintext .rb files |
| PHP | Rejected | Source code is plaintext .php files |
| Perl | Rejected | Source code is plaintext .pl files |
| Shell scripts | Rejected | Source code is plaintext; direct command injection vector |
| Java / Kotlin (JVM) | Rejected | .jar files contain decompilable bytecode; requires JVM runtime |
| .NET / C# (Mono) | Rejected | .dll assemblies contain decompilable IL; requires runtime |
The compiled-only policy is enforced at multiple independent points across two tiers:
NeboLoop performs expensive, thorough analysis once per upload before signing the binary. All binaries distributed through the app store have passed this gauntlet:
- Magic byte verification — Validates ELF (
\x7fELF), Mach-O (\xFE\xED\xFA\xCE/\xCF\xFA\xED\xFE/\xCA\xFE\xBA\xBE), or PE (MZ) headers - Hidden interpreter detection — Scans binary strings for
_PYINSTALLER,_pyinstaller_pyz,cpython,node.js,_MEItemp directory markers (PyInstaller extraction folders) - Dynamic link analysis — Uses ELF/Mach-O/PE section parsing to inspect shared library dependencies. Red flags that trigger rejection:
libpython,libnode,libjvm,libmono,libruby. These indicate a compiled wrapper around an interpreted runtime - Go build info verification — For Go binaries (the recommended language), reads Go module build info to confirm the binary was built with the Go toolchain
- Dropper pattern detection — Flags binaries that extract files to
/tmpat runtime (PyInstaller_MEIfolders, Node.js SEA extraction patterns) - ED25519 signing — Only binaries that pass all checks are signed. The signature is the trust anchor for Nebo's local verification
Nebo's local checks are intentionally lightweight and fast — magic bytes + signature is sufficient because signed binaries from NeboLoop have already passed deep validation:
- Extraction time (
napp.go): Validates magic bytes immediately after extracting the binary from a.napppackage. Rejects scripts (shebang#!detection) and non-native formats. Cleans up extracted files on failure - Launch time (
sandbox.go): Re-validates magic bytes before every process execution. Catches binaries replaced on disk between extraction and launch - Signature verification (
signing.go): ED25519 signature over raw binary bytes — any modification (even a single byte) invalidates the signature and prevents launch - File permissions: Binary installed as
0555(read + execute, no write), manifest and signatures as0444(read-only)
NeboLoop (deep, once per upload): Nebo (fast, every launch):
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ 1. Magic byte check │ │ 1. Magic byte check │
│ 2. Hidden interpreter scan │ │ 2. Shebang rejection │
│ 3. Dynamic link analysis │ │ 3. ED25519 signature verify │
│ 4. Go build info verify │ │ 4. File permission check │
│ 5. Dropper pattern detect │ └─────────────────────────────┘
│ 6. ED25519 sign if clean │
└─────────────────────────────┘
Sideloaded apps bypass NeboLoop and have no signature. Nebo still enforces:
- Magic byte validation (no scripts, no non-native formats)
- No signature verification (expected — developer mode)
- Warning displayed to user: "This app is unsigned and not verified by NeboLoop"
Modern operating systems enforce Write XOR Execute (W^X) — memory pages cannot be simultaneously writable and executable. This is an OS-level protection that Nebo relies on as an additional defense layer:
| Platform | Protection | How It Helps |
|---|---|---|
| macOS | Hardened Runtime + code signing required for execution | Even if the agent wrote a new binary to disk, macOS would refuse to execute it without a valid code signature (Gatekeeper). Apple Silicon enforces W^X in hardware via PAC (Pointer Authentication Codes) |
| Linux | mprotect W^X enforcement, SELinux/AppArmor |
Kernel prevents mapping memory as both writable and executable. Distribution-level policies (SELinux execmem denial) add further protection |
| Windows | DEP (Data Execution Prevention) + ASLR | Prevents execution of code in data pages. Modern Windows requires signed binaries for protected processes |
Why this matters for Nebo: Even in a worst-case scenario where a buffer overflow in a C/C++ app is exploited, the attacker cannot:
- Write shellcode to memory and execute it (W^X prevents this)
- Write a new binary to disk and execute it (signature verification prevents this)
- Modify the running binary's code in memory (code pages are read+execute only, not writable)
Nebo does not implement W^X — it is an OS guarantee we depend on and document as a security assumption. Apps that disable W^X protections (e.g., JIT compilers) would need explicit permission in the manifest.
A compiled binary that internally embeds an interpreter (e.g., a Go binary that embeds a Lua VM) is permitted — the outer binary is still a compiled, signed, opaque artifact. The embedded interpreter runs within the sandbox with no file system access to its own code. This is acceptable because:
- The agent cannot modify the embedded scripts (they're inside the compiled binary)
- The binary's signature covers the entire artifact including embedded resources
- The sandbox environment prevents the process from modifying its own binary
However, binaries that are wrappers around full interpreted runtimes are rejected:
- PyInstaller bundles a complete Python interpreter + bytecode → rejected by NeboLoop's deep scan (
_PYINSTALLERstring detection,libpythonlink check) - Node.js SEA (Single Executable Applications) embeds the V8 engine → rejected (
libnodelink check) - GraalVM native-image is an exception — it compiles to true native code with no interpreter, so it passes validation
This policy exists because Nebo occupies a unique position in software: it is an AI agent that runs third-party code on the user's machine. Traditional app sandboxing focuses on preventing apps from accessing unauthorized resources. Nebo must additionally prevent the agent from modifying the apps it runs. Compiled binaries achieve this by making app code opaque and immutable at the file system level.
The two-tier architecture (NeboLoop deep scan + Nebo fast verify) follows the same pattern as mobile app stores: Apple/Google perform deep analysis at submission time, devices do fast signature checks at install/launch time. This gives us the best of both worlds — thorough security without launch-time latency.
- JWT (HMAC-SHA256) for all API requests
- WebSocket authentication required — unauthenticated connections rejected with 401
- Password hashing: bcrypt with cost factor 12
- Password requirements: 8+ characters, uppercase, lowercase, digit, special character
- Random 32-byte tokens with HMAC-SHA256 signing
- Constant-time comparison (prevents timing attacks)
- 12-hour token expiry
SameSite: Strictcookies- Double-submit cookie option available
- Content-Security-Policy:
default-src 'self'; script-src 'self'; object-src 'none'; frame-ancestors 'none' - X-Frame-Options:
DENY(clickjacking prevention) - X-Content-Type-Options:
nosniff(MIME sniffing prevention) - Strict-Transport-Security: 1-year HSTS with preload
- Referrer-Policy:
strict-origin-when-cross-origin - Permissions-Policy: Blocks camera, microphone, geolocation, payment, USB
- Cache-Control:
no-store, no-cache, must-revalidate, private
- Whitelist-based origin validation (not
*) - Wildcard subdomain support
- Preflight method and header validation
- Token bucket algorithm, per-client
- General: 100 requests/minute, burst 20
- Auth endpoints: 5 requests/minute, burst 5
- Trusted proxy support (only trusts
X-Forwarded-Forfrom known proxy IPs) - Automatic cleanup of stale entries
- Maximum URL length: 2048 characters
- Maximum body size: 10MB
- Request size enforcement via
http.MaxBytesReader()
- SQL injection detection: 40+ regex patterns covering comments, keyword injection, boolean injection, time-based injection, hex encoding, null bytes, stacked queries
- XSS detection: HTML tag and script pattern matching
- Identifier validation: Alphanumeric + underscore only, max 128 chars, reserved word blocking
- Sanitization: HTML entity escaping, control character removal, null byte stripping, UTF-8 validation
All database queries use sqlc-generated parameterized queries (prepared statements). No raw string concatenation in SQL.
- All credentials encrypted with AES-256-GCM: API keys, OAuth tokens, plugin settings (
internal/credential/credential.go) - MCP tokens: AES-256-GCM with random nonces (
internal/mcp/client/crypto.go) - Key derivation from
MCP_ENCRYPTION_KEY,JWT_SECRET, or persistent key file (0600permissions) - Idempotent migration encrypts unencrypted values on startup (single transaction, rollback on failure)
Only one Nebo instance per computer. Uses filesystem locks (flock on Unix, LockFileEx on Windows) with PID tracking.
- Each app runs as a separate process with its own Unix domain socket
- Sanitized environment (no inherited secrets)
- Per-app log files with restricted permissions
- Health checks with timeouts
- Maximum message size: 32KB
- Read deadline: 60 seconds (with ping/pong keepalive)
- Buffered send channel (256 messages)
- Graceful close on connection drop
This section tracks every security vulnerability identified during development, how each was resolved, and which remain open. Vulnerabilities are sourced from internal audits, the OpenClaw comparison analysis, and the MAESTRO threat framework.
Status: CLOSED | Files: internal/middleware/cors.go, internal/agenthub/hub.go, internal/websocket/handler.go
Both WebSocket endpoints (/ws, /api/v1/agent/ws) had CheckOrigin: return true, allowing any webpage to establish connections and control the agent remotely.
Hardening: Added IsLocalOrigin() helper to middleware/cors.go that validates the Origin header against localhost/127.0.0.1. Empty Origin is allowed for direct CLI/agent connections. Applied to both the agent WebSocket (agenthub/hub.go) and chat WebSocket (websocket/handler.go). The gateway proxy was left unchanged because it uses separate token-based device authentication. Browser relay CDP upgraders already had localhost validation.
Status: CLOSED | Files: internal/agent/tools/shell_tool.go, internal/agent/tools/shell_unix.go, internal/agent/tools/process_registry.go
bash -c ran directly on the host with no sandbox. No environment variable sanitization — PATH, LD_PRELOAD, DYLD_INSERT_LIBRARIES all inherited from the parent process. Policy allowlist used prefix matching bypassable with semicolons.
Hardening: Added sanitizedEnv() function that strips 30+ dangerous environment variables before every shell execution:
- Linux linker injection:
LD_PRELOAD,LD_LIBRARY_PATH,LD_AUDIT, and allLD_*prefixed vars - macOS linker injection:
DYLD_INSERT_LIBRARIES,DYLD_LIBRARY_PATH,DYLD_FRAMEWORK_PATH, and allDYLD_*prefixed vars - Shell behavior manipulation:
IFS,CDPATH,BASH_ENV,PROMPT_COMMAND,SHELLOPTS,BASHOPTS,GLOBIGNORE - ShellShock-style function exports:
BASH_FUNC_*prefix - Interpreter injection:
PYTHONSTARTUP,NODE_OPTIONS,RUBYOPT,PERL5OPT,PERL5DB - DNS manipulation:
HOSTALIASES,RESOLV_HOST_CONF
Applied to both foreground (handleBash) and background (SpawnBackgroundProcess) execution paths. ShellCommand() now uses absolute /bin/bash path with fallback to /usr/bin/bash and /usr/local/bin/bash, preventing PATH-based binary substitution attacks. 5 new tests pass.
Status: CLOSED | Files: internal/agent/tools/web_tool.go
web_tool.go handleFetch accepted any URL with no validation — no checks for private IPs, localhost, cloud metadata endpoints, or non-HTTP schemes. Default http.Client followed redirects to internal IPs.
Hardening: Added comprehensive SSRF protection with 3 layers:
- Pre-flight validation (
validateFetchURL()): Validates URL scheme (http/https only), resolves hostname via DNS, blocks all private/internal IP ranges (127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,0.0.0.0/8,100.64.0.0/10,::1/128,fc00::/7,fe80::/10) - Connection-time validation (
ssrfSafeTransport()): Custom dialer re-validates resolved IPs at connection time, catching DNS rebinding attacks and redirect-based SSRF - Redirect validation (
ssrfSafeRedirectCheck()): Validates every redirect target against the same rules, with a 10-redirect cap
25 test cases covering public URLs, all blocked IP ranges, scheme validation, and edge cases.
Status: CLOSED | Files: 15 files across codebase
All references to local.nebo.bot posed a DNS hijack risk — if anyone gained control of the nebo.bot domain, they could serve malicious pages passing CORS/origin checks on every Nebo instance.
Hardening: Replaced all references to local.nebo.bot with localhost across 15 files. Default domain is now localhost, default BaseURL is http://localhost:27895. Removed from: config defaults, vite dev server, server config, env example, browser relay origin check, chrome extension manifest and storage, agent config, provider CLI output, profile handler, default config.yaml, documentation.
Status: CLOSED | Files: internal/agent/tools/origin.go, internal/agenthub/lane.go, internal/agent/runner/runner.go
Without origin tagging, there was no way to distinguish a user typing a command from a remote MQTT message or app output triggering the same command.
Hardening: Implemented origin tagging system with 5 origin types (user, comm, app, skill, system). Every request carries an origin through context propagation (WithOrigin(ctx, origin) / GetOrigin(ctx)). The origin is set at the entry point (WebSocket handler, MQTT listener, app adapter, skill matcher, cron scheduler) and flows through the entire execution chain.
Status: CLOSED | Files: internal/agent/tools/policy.go, internal/agent/tools/registry.go
registry.Execute() only checked policy level (full/approve/readonly) but not who was requesting. Remote agents, apps, and skills could access any tool.
Hardening: Extended the tool registry to check session origin before allowing tool execution. Policy.IsDeniedForOrigin() is checked before any approval logic. Configurable deny lists per origin with secure defaults.
Status: INFRASTRUCTURE READY, DENY LIST PENDING | Files: internal/agent/tools/policy.go
Without default restrictions, comm/app/skill origins have full access to shell, file writes, memory, cron, and sub-agent spawning.
What's done: Policy.IsDeniedForOrigin() is checked in registry.Execute() before any approval logic. The deny list is configurable per origin per tool via config.yaml. The infrastructure is fully wired and tested.
What's pending: defaultOriginDenyList() currently returns nil — no deny rules are active. The intended defaults:
- comm/channel origins: Shell (all actions) denied — neutralizes NeboLoop as a remote shell vector
- app origins: Shell (all actions) denied — app outputs re-entering the loop cannot escalate
- skill origins: Shell (all actions) denied — malicious skill templates cannot trigger dangerous tools
Re-enabling is a one-function change. Blocked on a review of which specific actions each origin should be denied to avoid breaking legitimate use cases.
Priority: High | Files: internal/credential/credential.go, internal/credential/migrate.go, internal/mcp/client/crypto.go
All credentials are encrypted at rest using AES-256-GCM:
auth_profiles— LLM provider API keys (Anthropic, OpenAI, Google, NeboLoop)mcp_integration_credentials— MCP OAuth tokensapp_oauth_grants— app OAuth tokensplugin_settings— plugin credentials (e.g., NeboLoop token)
Encryption is applied via migrateAuthProfiles() which runs on startup and idempotently encrypts any unencrypted values (detected by absence of "enc:" prefix). Migration runs in a single database transaction with rollback on failure.
Remaining concern: The encryption key sits on disk at ~/.config/nebo/.mcp-key (permissions 0600). If an attacker has filesystem access, they can read both the database and the key file. The proper fix is OS Keychain integration (macOS Keychain, Windows DPAPI, Linux libsecret), which ties credential access to the user's login session.
Priority: Critical | Files: internal/browser/relay.go
CDP endpoint allows unauthenticated loopback connections. Any local process can take full browser control, steal cookies, extract session tokens. Needs auth token requirement for all connections, connection rate limiting, and CDP command logging.
Priority: Critical | Files: internal/agent/tools/web_tool.go, internal/browser/snapshot.go
web_tool.go returns raw HTML to the LLM with only length truncation, no sanitization. No HTML stripping, no script removal. Malicious webpages can inject instructions into agent context. Needs HTML tag stripping, script/style removal, and special character escaping before returning content to the LLM.
Status: CLOSED | Files: internal/agent/tools/file_tool.go
file_tool.go read/write/edit had no path restrictions. The agent could read ~/.ssh/id_rsa, ~/.aws/credentials, /etc/passwd. Write could target ~/.bashrc, ~/.ssh/authorized_keys. No symlink protection. Dangerous-path checks only existed for grep, not read/write/edit.
Hardening: Added validateFilePath() with a sensitivePaths blocklist (~25 paths including ~/.ssh, ~/.aws, ~/.config/gcloud, ~/.azure, ~/.gnupg, ~/.docker/config.json, ~/.kube/config, ~/.npmrc, ~/.password-store, ~/Library/Keychains, browser profiles, shell rc files, /etc/shadow, /etc/passwd, /etc/sudoers). Symlink resolution via filepath.EvalSymlinks prevents symlink-based traversal. pathMatchesOrIsInside() helper checks both exact match and directory containment. Applied to handleRead, handleWrite, and handleEdit. 12 tests pass.
Status: CLOSED | Files: internal/handler/auth/devloginhandler.go (deleted)
Nebo-specific issue. /api/v1/auth/dev-login returned valid JWT for hardcoded test emails without password verification.
Hardening: Deleted entirely rather than gating behind an environment variable. The existing auth system (register, login, setup wizard) covers all use cases. Removed: handler, route from server.go, frontend page, API client function. Ran make gen to confirm clean regeneration.
Status: PARTIALLY CLOSED | Files: internal/apps/signing.go, internal/apps/runtime.go, internal/apps/registry.go, internal/apps/install.go
What's done (App signing):
- ED25519 signature verification over raw bytes for both manifest and binary
SigningKeyProviderwith 24-hour cache TTL and force-refresh on failureRevocationCheckerwith 1-hour cache TTL- Runtime enforcement: Launch() checks revocation → verifies signatures → validates binary before executing
- Quarantine mechanism: stops process, removes binaries, preserves data/logs, writes
.quarantinedmarker - Periodic revocation sweep: background goroutine checks all running apps every hour
- MQTT
app_revokedevent handler for immediate kill switch - Comprehensive tests with real ED25519 keys
What's still needed: Skills signature verification in internal/agent/skills/loader.go, wiring NeboLoopURL to AppRegistry in cmd/nebo/agent.go.
Priority: High | Files: internal/agent/memory/dbcontext.go, internal/agent/memory/extraction.go
memory/dbcontext.go concatenates stored memories directly into the system prompt with zero escaping. Time-shifted prompt injection is viable — an attacker stores a malicious payload in memory, it activates later. Needs sanitization of all stored values, structured delimiters (XML/JSON tags), and content-type validation for extracted memories.
Priority: High | Files: internal/agent/runner/compaction.go, internal/agent/session/
Compaction summaries are prepended to the system prompt for future turns. A poisoned conversation (via prompt injection from web content, comm messages, or app output) gets compressed into a summary that persists across sessions. Needs structured state snapshots instead of free-form prose, a summary sanitizer, provenance tracking, and fallback behavior for suspicious patterns.
Status: CLOSED | Files: internal/middleware/ratelimit.go, internal/middleware/ratelimit_test.go
middleware/ratelimit.go blindly trusted X-Forwarded-For for rate limit keying. Attackers could bypass auth rate limits (5 attempts/min) by rotating the header value.
Hardening: DefaultKeyFunc now uses only r.RemoteAddr, ignoring X-Forwarded-For and X-Real-IP entirely since any client can spoof these headers. Added TrustedProxyKeyFunc(trustedProxies []string) for deployments behind a known reverse proxy — it only trusts forwarded headers when RemoteAddr matches a whitelisted proxy IP. Added splitHostPort helper for IPv4/IPv6 address parsing. 10 rate limiter tests pass.
Status: CLOSED | Files: internal/middleware/cors.go, internal/middleware/security.go
Security middleware previously used an empty AllowedOrigins list, providing no explicit allowlist.
Hardening: DefaultCORSConfig now returns localhost-only origins (localhost:27895, localhost:5173). Removed local.nebo.bot to prevent DNS hijack attacks. Added BaseURL-derived origin fallback in security.go for production deployments. Three tiers: explicit AllowedOrigins config > BaseURL-derived > localhost defaults.
Priority: Medium | Files: internal/server/server.go, internal/middleware/chi_jwt.go
Any local process can hit the Nebo HTTP API on port 27895. While JWT auth exists on protected routes, a full audit is needed to verify: all endpoints require auth tokens, no endpoints rely solely on origin/IP checks, WebSocket upgrade endpoints require valid JWT, and static file serving doesn't leak sensitive data.
Priority: Medium | Files: internal/agent/orchestrator/orchestrator.go, internal/agenthub/lane.go
The subagent lane has no concurrency limit (set to 0 = unlimited). A comm-origin task or prompt injection could spawn many sub-agents to brute-force past restrictions or consume resources. Needs max concurrent sub-agent caps, per-origin rate limits, wall clock time caps for comm-origin sessions, and total spawn count limits.
Priority: Low | Files: To be determined
Prompt injection via file uploads and media — hidden text in image EXIF metadata, invisible PDF text layers, zero-font-size text, CSS-hidden injection payloads. Needs metadata stripping, visible-text-only extraction, and source markers for media-derived content.
Priority: Low | Files: internal/db/
SQLite stores logs with no integrity protection. An attacker with filesystem write access could modify or delete log entries. Planned approach: append-only triggers, hash-chained log entries (SHA-256), and eventual remote log shipping to NeboLoop.
If you discover a security vulnerability, please report it responsibly:
- Do not open a public GitHub issue
- Email security concerns to the maintainers directly
- Include steps to reproduce the vulnerability
- Allow reasonable time for a fix before disclosure
- Defense in depth — Multiple independent layers, each assuming others might fail
- Fail closed — When in doubt, block the operation
- Least privilege — Deny by default, require explicit opt-in
- No silent failures — Blocked operations explain why and what the user can do instead
- Unconditional safety — Critical protections (system paths, sudo, disk formatting) cannot be overridden by any setting, mode, or configuration