owlwarden is a security tool. That raises the bar: a weakness in the scanner is worse than a weakness in an ordinary library, because people run it against code they do not fully trust, and because a false sense of coverage is itself a vulnerability.
Report through GitHub — prefer a private security advisory so the details are not public until a fix is out:
Do not open a public issue for anything that could be exploited. Ordinary bugs and false positives belong on Issues; see CONTRIBUTING.md.
Include:
- a description of the issue
- steps to reproduce, or a proof of concept
- the affected version (or commit)
- any ideas you already have about a fix
You should hear back within two working days. If the report is in scope we will coordinate a fix and a disclosure date with you; we do not ask for an embargo longer than 90 days.
In scope:
- anything that lets a hostile scan target escape the sandbox, execute code in the scanner process, overwrite files outside the intended write path, or exhaust resources past the documented caps
- anything that causes owlwarden to send data off the machine without an explicit, documented action from the operator
- supply-chain issues in our published packages (tampered binaries, unexpected
postinstallbehaviour, missing provenance)
Out of scope:
- Findings owlwarden misses. A missed vulnerability is a bug — file it as one — but it is not a vulnerability in owlwarden.
- False positives. Also bugs, also not security issues, and we want them reported: see CONTRIBUTING.md.
- Attacks that require the attacker to already be able to run code as you before the scan starts (compromised CI runner, malicious shell profile). A scan target that becomes code execution by being scanned is in scope.
A hostile scan target. Someone runs owlwarden against a repository designed to attack the scanner (a pull request from an outsider, a cloned tree, a fixture).
- Executable project config is opt-in.
owlwarden.config.{js,mjs,ts,mts}is onlyimport()ed when the operator passes--allow-config-js. The default loads JSON (and theowlwardenkey inpackage.json) only — so placing a config module in a PR cannot get code execution. Never pass--allow-config-json an untrusted tree. --ciignores project mute switches. Under--ci, projectpreset/failOn/minConfidenceare ignored unless--allow-project-config; inline suppressions are listed but not applied unless--allow-suppressions;--baselineis refused unless--allow-baseline. Pin gate flags on the command line in CI.- Every response and every source file has a byte cap; reads use a bounded
Read::take(andO_NOFOLLOWon Unix) so a file that grows or is swapped for a symlink under our feet cannot pull unbounded or out-of-tree bytes. - Every loop over external data has an explicit bound (
limits.rs). - Deeply nested source is rejected before it reaches the parser, with a scan that skips comments/strings and counts generics/JSX — see ADR 0008.
- Unparseable and oversized files are skipped and reported, never fatal.
--outand--write-baselinewrite via temp-file +rename, so a planted symlink at the destination is replaced rather than followed. They also refuse when any ancestor directory is a symlink, so a linked--outparent cannot redirect the write outside the intended tree.- Hitting the findings cap sets
truncated: trueand fails CI — a partial report is never treated as a clean scan. - Config and baseline loads use
lstat/ refuse symlinks and oversized inputs before parse.
A hostile plugin. Shipped (v0.2), source-only. Plugins run in wasmtime with
no WASI, no filesystem, no network, no clock. Fuel, linear-memory
StoreLimits, table-element caps, and a wall-clock epoch budget bound each
invocation. Manifests that declare network / active are refused at load.
Rule ids must be namespaced under the plugin id; confirmed confidence is
refused for source-only plugins; emit_finding re-validates every claim and
strips control/invisible characters from why (prompt-injection hygiene).
--plugin under --ci requires --allow-plugins. Treat third-party plugins
like any other code you execute: only load ones you trust. See
ADR 0015 and
crates/plugin-host/tests/sandbox_escape.rs.
A hostile scan target talking to an agent. Findings and snippets are fed to
coding agents via MCP / JSON. MCP wraps every tool result as untrusted DATA
and neutralises common role markers; init --agent-rules tells agents not to
obey instructions embedded in findings. This reduces confusion with the host
prompt — it does not make a model immune to social-engineering text in source.
Supply chain. A dependency of owlwarden, or of its build, is compromised.
- Lockfiles are committed.
cargo-denyandcargo-auditrun in CI. - npm install scripts are blocked by default; the allowlist is in
pnpm-workspace.yamland is reviewable in a diff. - Native addons are prebuilt and published with npm provenance. Nothing is downloaded at install time.
- New dependencies need a justification in the PR, not just a green build.
Accidental disclosure by the tool itself. A scanner that prints secrets in its own output has made things worse.
- Findings do not include secret values in
evidence, andhardcoded-secretredacts the value insidesnippet.linesas well (a short prefix remains for triage). - Nothing is transmitted anywhere. There is no telemetry to opt out of.
- Missed findings and false positives — product bugs, not security issues in the tool.
- Concurrent writers racing the scanner on a shared volume after the process
has already started, beyond what
O_NOFOLLOW/ bounded reads already cover.
Without --target, owlwarden is static-only: it reads source and sends no
requests. With --target, it issues passive probes (GET/HEAD/OPTIONS) to
that URL under a deny-by-default scope allowlist. It still cannot change the
target's state — active methods stay behind --allow-active, and no active
detector ships yet.
--target and --scope come from the command line only, never from a
file inside the scanned tree. A hostile pull request therefore cannot point the
scanner at an internal host via project config.
When pointing the npm CLI at a tree you do not trust (for example, CI on an external pull request):
npx owlwarden scan --ci --fail-on medium --min-confidence likely
# do NOT add --allow-config-js, --allow-project-config,
# --allow-suppressions, or --allow-baseline
# If you pass --target, you chose the host — still never trust config for it.The standalone native binary never loads executable JS config at all.
Active checks will remain behind an explicit --allow-active flag and a
declared scope allowlist. Scope is deny-by-default, including every redirect
hop (ADR 0014).
These are accepted for 0.2.0 and documented rather than papered over:
- DNS rebinding. Scope matches the hostname (or IP literal) you named, not
the resolved address after connect. An operator who allowlists a hostname
they do not control can be rebound to another address on a later hop. Prefer
IP literals for local probes (
http://127.0.0.1:3000/), and do not point--targetat untrusted DNS. - Operator-chosen target.
--targetcan reach anything the runner can route to. That is intentional — and why the URL never comes from project config. Treat the flag like a curl destination.
Hardening that is enforced: credentials in URLs refused; Location re-parsed
through the same validator (blocks user@host confusion, javascript:, and
oversized values); protocol-relative redirects scope-checked; response header
values capped; headers-only probes do not buffer a body; request header CRLF
rejected.
The engine follows the same discipline we ask of security-critical code elsewhere in the project (and tracks the spirit of NASA’s Power of Ten rules where they apply to a CLI tool rather than flight software):
- Bound every loop over external data — explicit
.take(N)or a documented cap incrates/core/src/limits.rs(files, findings, redirects, MCP lines, plugin fuel/memory/tables, snapshot size). - No
unwrap/expect/panic!in library paths — typed errors only; tests may panic. - Validate at the boundary — paths, URLs, manifests, guest findings, MCP JSON-RPC lines.
- Fail closed on trust — deny-by-default scope;
--cimute switches off unless opted in; plugins refused under CI without--allow-plugins. - Keep functions short and reviewable — extract rather than grow a 200-line path that mixes I/O and policy.
#![forbid(unsafe_code)]in library crates; the only exception isplugin-host, which isolates all wasmtime use in one crate.
These are checked in review and in CI (pnpm check), not only in docs.
Only the latest released version receives security fixes. Pre-1.0, that means
the current 0.x line on npm and on GitHub Releases.