docsxai is an LLM-agnostic documentation engine plus a Claude Code plugin and a small authenticated backend. Its threat surface is shaped by two facts: the engine ships no model-API surface and no arbitrary-code-execution surface — it parses flow-files against a Zod schema, drives Playwright through a curated step vocabulary, and emits screenshots and text — and the backend is a stateless HTTP service bound to loopback by default. This document tells you what we will and will not commit to, and how to report a vulnerability.
docsxai is pre-v1.0. 0.2.0 is published on npm: the bare
docsxai package (the batteries-included CLI meta-package over
@docsxai/engine + @docsxai/viewer) alongside @docsxai/{engine,viewer,plugin,skill,backend}
(see RELEASING.md). The support windows below start at v1.0; until then:
| Version range | Support level |
|---|---|
0.x (pre-v1.0) |
Versions before v1.0 carry no security guarantees. security@kalebtec.com still triages reports in good faith and ships fixes on a best-effort basis on the active branch. |
1.(latest).x (post-v1.0) |
Patches for any qualifying vulnerability. |
1.(latest-1).x |
Critical only. |
1.(latest-2).x and older |
No support. Upgrade. |
"Critical" means: remote code execution, secrets exfiltration (auth- strategy cache leak), workspace escape, or backend auth bypass. Everything else is "qualifying."
Please do not open a public issue for security reports.
Primary channel: GitHub Security Advisories. From the docsxai repository, open the Security tab and choose "Report a vulnerability". This creates a private advisory thread visible only to you and the maintainers.
Fallback channel: email security@kalebtec.com with the subject
prefix [docsxai-security]. Use this only if GitHub Security Advisories
is unavailable to you.
Please include:
- docsxai version (output of
docsxai --versionor the relevant package'spackage.json). - Component (engine / plugin / backend / viewer / skill).
- Node version and OS.
- Minimal reproduction: a flow-file fragment, the CLI invocation, a backend request payload, or a viewer-loadable doc pack.
- Your assessment of impact and severity.
- Acknowledgement: within 48 business hours.
- Initial assessment: within 7 calendar days. We tell you whether the report is in scope, the severity we assign, and the patch path.
- Critical patch target: 30 days from confirmed report.
- Lower-severity patch target: 90 days from confirmed report.
- Coordinated disclosure: patch-then-disclose is the default. We publish the advisory once a fixed version is released and adopters have a window to upgrade. Embargo will not exceed 90 days from confirmed report without your written agreement.
- Credit: reporters are credited in the CHANGELOG entry and the GitHub Security Advisory by name and (optional) affiliation, unless you ask to be omitted.
Reports against the following are in scope:
- Workspace-escape path traversal — any engine path that reads or
writes outside the configured workspace root via path traversal,
symlink follow, or
..segments. The workspace boundary is the filesystem trust boundary; the engine's job is to not cross it. - Auth-strategy cache leak — cached cookies, tokens, or auth headers appearing in any externally-visible sink (logs, doc-pack artifacts, viewer output, halt context, zip output, screenshots).
- Flow-file schema bypass — any malformed flow-file payload reaching the runtime past the Zod schema boundary, especially anything that shifts behavior into arbitrary code execution.
- In-page script injection — any path that ships agent-supplied or
flow-file-supplied JavaScript into the page beyond the curated step
vocabulary. The engine has no
eval-shaped step; introducing one without an explicit threat-model entry is in scope. - Backend auth bypass — any request reaching authenticated backend endpoints without a valid OAuth 2.1 credential, or any path that serves one tenant's doc pack to another.
- Viewer XSS / HTML injection — any flow-file or annotation content reaching the rendered viewer as live HTML rather than escaped text.
- Loopback escape — the backend defaults to loopback binding; any default configuration that exposes it on a non-loopback interface unintentionally is in scope.
- Backend
app_urltarget filter and engine request guard — a webhook run screenshots whatever a project'sapp_urland its flows'navigatesteps name, so a few checks stand between a tenant and a cloud instance's credentials or the host's files. A way around one to reach a refused address, or to upload a file from outside the workspace, is in scope.- Backend, when a project is saved and when a run is materialised.
app_urlmust be an absolutehttp(s)URL without credentials whose host is not link-local or a cloud-metadata address:169.254.0.0/16,fe80::/10,fd00:ec2::254,100.100.100.200,168.63.129.16,metadata.google.internal,instance-data, in any spelling the URL parser normalises (decimal, hex or octal IPv4, trailing dot) and as an IPv6 address that embeds one (IPv4-mapped, NAT6464:ff9b::/96and64:ff9b:1::/48, 6to42002::/16, Teredo2001::/32with the client address decoded). WithDOCSX_BACKEND_DENY_PRIVATE_APP_URLset to1,trueoryesit also refuses loopback, RFC 1918, CGNAT, unique-local,fec0::/10and the nameslocalhost,localhost.localdomain,*.localhost. Materialising a revision also reads every flow file and refuses anavigatevalue that resolves to an absolute URL those rules refuse, or to one outside theapp_urlorigin when the project has anapp_url. A relative value passes, and so does a public absolute URL when there is noapp_url. This check is on the host as written: a DNS name is not resolved, so169.254.169.254.nip.iopasses it. - Engine request guard, off by default. With
DOCSX_EGRESS_GUARDset to1,trueoryes,launchPlaywrightSessionroutes every request of the browser context through a check: documents, subresources, fetches and each redirect hop (Chromium reports a redirect as a new request). The host is classified with the rules above and, when it is a name, resolved withdns.lookupfor all its addresses; one refused address, a failed lookup or an empty answer aborts the request.DOCSX_EGRESS_DENY_PRIVATEset to1,trueoryesadds the private ranges. A name lookup that takes longer than 5 seconds refuses the request, so a stalled resolver cannot hold one open. WebSocket connections go through the same check (context.routeWebSocket), and a refused one is closed. Schemes other thanhttp,https,ws,wss,data,blobandaboutare aborted, and service workers are blocked so their fetches cannot skip the route. What the run prints for a refusal is generic (egress-guard: blocked <url without userinfo, query or fragment>: address not allowed), because the runner's last output line is shown to the tenant; the host and resolved address go only to theonBlockcallback. The backend runner turns the guard on for every webhook run unlessDOCSX_EGRESS_GUARDis0,falseorno, and turns the private ranges on whenDOCSX_BACKEND_DENY_PRIVATE_APP_URLis on. A localdocsxai runhas the guard off unless the variable is set. - Uploads. An
uploadstep's value must be a path relative to the workspace, with no..segment and no segment that starts with a dot (so no.auth/session or.docsxai.json), notwebhook-job.json, naming a regular file whose real path is under the workspace root; the engine's driver refuses anything else when the step runs, and the backend refuses a flow file with such a value when it materialises the revision. - Not covered. On a browser attached over CDP (
--cdp) the context belongs to the operator's Chrome, so the guard cannot block service workers there; their fetches skip the route. The guard resolves a name and Chromium resolves it again when it connects, so a DNS answer that changes between the two (DNS rebinding) gets through; that window remains. Redirect coverage relies on Chromium reporting each hop to the route handler. A hosted deployment needs an egress firewall or network policy on the host that runs the engine that drops traffic to link-local and private ranges, whatever these switches say.
- Backend, when a project is saved and when a run is materialised.
- Plugin manifest trust bypass — any path through the Claude Code plugin that escalates beyond the declared plugin permissions.
The following are not docsxai vulnerabilities:
- Vulnerabilities in upstream Playwright, Chromium, Node.js, or Claude Code that do not also have a docsxai-specific code path. Report those to their respective projects.
- Findings that require an attacker who already controls the operator's shell, workspace, or auth-strategy cache. We do not defend against local-root attackers.
- Social engineering, physical access, and denial-of-service via resource exhaustion on the operator's own machine.
- Issues in adopter-side configuration (exposing the backend beyond loopback on an untrusted network, running docsxai with elevated OS privileges, calibrating against a hostile target site).
- Issues in third-party flows or doc packs distributed by adopters. Report those to the doc pack's owner.
What we promise. The published @docsxai/* packages,
installed from npm with provenance verified (npm audit signatures),
have no built-in code-execution surface beyond what Playwright itself
exposes through the curated step vocabulary. The engine never imports
a model-provider SDK, and that constraint is a contract violation if
broken. The flow-file parser validates against a Zod schema before
anything runs. The backend defaults to loopback. No lifecycle script
runs at install time.
What we DO NOT promise. docsxai operates on whatever target site the operator points it at. The engine cannot tell whether the target site is hostile. If the operator calibrates against a malicious site: that site cannot escape the page (Playwright + Chromium do their job), but the operator's screenshots, captured text, and any auth tokens cached for that target ARE controlled by the operator's input. The operator is responsible for the trust posture of the targets they point the engine at.
What we cannot prevent. If an adopter installs a typosquat
(docsxa, dcosxai, docsx-ai, site-doc, etc.) instead of
docsxai / @docsxai/*, our defenses do not apply. Verify
the package name and scope before install; verify provenance after
install. See docs/security-best-practices-for-adopters.md (lands
with the launch-gate phase).
The MCP HTTP transport. docsxai-mcp serve --http is opt-in; the default transport is stdio.
It requires a bearer token of at least 32 characters, taken from the environment or a file and never
from an argument, and compares it in constant time. It refuses a token with fewer than 8 distinct
characters and, on POSIX, a token file that group or other can access, and it clears the variable
from its environment after reading so spawned tool processes do not inherit it. --token-file is the preferred source: the initial environment block of the process stays readable to other processes of the same user, and clearing the variable does not change that. On Windows the token file's mode cannot be checked, and the server warns on stderr. It binds loopback unless --allow-remote is
passed, and then it speaks plain HTTP, so TLS must terminate in a proxy in front of it. It checks the
Host and Origin headers against loopback names and an operator-supplied list to block DNS
rebinding (the Host value is parsed strictly as host[:port], and an Origin must use the bound port
or match an operator-supplied origin), and it caps request bodies at 1 MiB, concurrent sessions at 16 and idle time at 30
minutes. It refuses to start without --workspace-root and confines every path a tool receives to
that directory, symlinks resolved. Refusals return a bare status code with no detail. It does not rate-limit failed tokens. A
holder of the token can call every tool the server exposes, including run_flows, push_pack and
pull_pack, inside the workspace root and with the server process's environment. Over HTTP, run_flows and diagnose_halt refuse cdp, run_flows refuses a baseUrl that is not an http(s) URL or whose host is or resolves to a refused address (the engine's address rules), and every browser the tools start runs with the engine request guard on; over stdio none of that applies.
The Claude Code plugin (@docsxai/plugin) runs inside Claude
Code's plugin sandbox and delegates execution to the engine binary —
it does not embed its own runtime. Plugin skills emit structured
questions and shell out to docsxai; plugin commands are
deterministic engine invocations. The plugin does not introduce a new
code-execution surface beyond what the engine already exposes.
The @docsxai/skill package is an optional vendorable
.claude/skills/ fallback that delegates to the installed plugin
for teams that prefer version-pinning in the consumer repo. The same
trust posture applies — skill is a thin redirect, not a parallel
runtime.
Third-party docsxai integrations are not reviewed by Kalebtec. Report issues in them to their maintainers.
None. docsxai is a solo-maintained Apache-2.0 project with no hosted offering at this stage and no monetisation. We cannot pay bounties. We can and do credit reporters publicly (with consent) and coordinate disclosure seriously.
The repository enforces a strict GitHub App allowlist; bots commonly
installed for security scanning (Snyk, Sonatype, Mend, Socket.dev,
Codecov, etc.) are not installed and will not be invited. The
rationale: each installed App expands the trusted-write surface to a
third party whose own compromise becomes our compromise, and the
findings these tools surface are already covered by first-party CI
(pnpm audit, trufflehog secret scanning, license-checker, lockfile
lint). Once the bot allowlist artifact lands in .github/, that file
holds the canonical policy. Adding a new App that requires write
access requires explicit owner approval and a rationale entry there.