Skip to content

Security: kalebteccom/docsxai

Security

SECURITY.md

Security Policy

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.

Supported versions

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."

Reporting a vulnerability

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 --version or the relevant package's package.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.

What to expect

  • 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.

In scope

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_url target filter and engine request guard — a webhook run screenshots whatever a project's app_url and its flows' navigate steps 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_url must be an absolute http(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, NAT64 64:ff9b::/96 and 64:ff9b:1::/48, 6to4 2002::/16, Teredo 2001::/32 with the client address decoded). With DOCSX_BACKEND_DENY_PRIVATE_APP_URL set to 1, true or yes it also refuses loopback, RFC 1918, CGNAT, unique-local, fec0::/10 and the names localhost, localhost.localdomain, *.localhost. Materialising a revision also reads every flow file and refuses a navigate value that resolves to an absolute URL those rules refuse, or to one outside the app_url origin when the project has an app_url. A relative value passes, and so does a public absolute URL when there is no app_url. This check is on the host as written: a DNS name is not resolved, so 169.254.169.254.nip.io passes it.
    • Engine request guard, off by default. With DOCSX_EGRESS_GUARD set to 1, true or yes, launchPlaywrightSession routes 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 with dns.lookup for all its addresses; one refused address, a failed lookup or an empty answer aborts the request. DOCSX_EGRESS_DENY_PRIVATE set to 1, true or yes adds 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 than http, https, ws, wss, data, blob and about are 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 the onBlock callback. The backend runner turns the guard on for every webhook run unless DOCSX_EGRESS_GUARD is 0, false or no, and turns the private ranges on when DOCSX_BACKEND_DENY_PRIVATE_APP_URL is on. A local docsxai run has the guard off unless the variable is set.
    • Uploads. An upload step'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), not webhook-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.
  • Plugin manifest trust bypass — any path through the Claude Code plugin that escalates beyond the declared plugin permissions.

Out of scope

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.

Trust posture — what we promise, what we do not, what we cannot prevent

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.

Plugin trust model

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.

Bug bounty

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.

Bot allowlist policy

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.

There aren't any published security advisories