sdo-cli ships the sdo binary, the official command-line interface for Scrape.do. It also bundles a Model Context Protocol (MCP) server, started with sdo mcp serve, that exposes Scrape.do operations as tools to MCP-capable hosts. This document describes how to report vulnerabilities and the security properties you can rely on.
Security fixes land on the latest released minor version. We recommend always running the most recent release of the sdo binary.
| Version | Supported |
|---|---|
| Latest minor release | Yes |
| Older releases | No — please upgrade |
Do not open a public issue for security problems.
Report privately through GitHub's Security Advisories on this repository. If you cannot use that channel, email security@scrape.do with:
- A description of the issue and its impact.
- The exact
sdoversion (sdo version), OS, and architecture. - Minimal steps or a proof of concept to reproduce.
- Whether the issue is in the CLI itself, the bundled MCP server (
sdo mcp serve), or token handling.
- Acknowledgement within 3 business days.
- Triage and severity assessment within 7 business days.
- A coordinated fix and disclosure timeline communicated directly to you.
- Credit in the advisory and release notes, unless you ask to remain anonymous.
Governance is Solo + AI: the founder is the sole approver of every change, including security fixes. Contributions are accepted under DCO sign-off (Signed-off-by trailer), not a CLA. There is no committee and no external maintainer who can independently merge a fix; this keeps the trust boundary small and auditable.
Please give us a reasonable window to ship a fix before any public disclosure.
sdo authenticates to the Scrape.do API with a single API token. There is no username/password, OAuth, or session cookie — token-only.
- Precedence:
--token> theSDO_TOKENenvironment variable > the per-user config file written bysdo auth login.sdo auth statusnames the source in use and never prints the token. - Avoid
--tokenon shared machines. Flag values are visible in the process list (ps,/proc) and are commonly captured in shell history. Prefersdo auth loginorSDO_TOKENfrom a secrets manager. - The config file lives at
<user config dir>/sdo/config.json($XDG_CONFIG_HOMEor~/.configon Linux,~/Library/Application Supporton macOS,%AppData%on Windows), or atSDO_CONFIG_FILE.sdo auth loginreads the token without echoing it when stdin is a terminal, creates the directory with mode0700(and narrows the default<user config dir>/sdoto0700if you own it and it is wider), and writes the file atomically: to a new mode-0600file in the same directory that is then renamed over the target, so the token never sits in a file others can read, even if one existed with wider permissions. It refuses a config path that is a symlink or not a regular file, on both write and read, and refuses to read a file readable by group or others; on Unix the file is opened withO_NOFOLLOWand its type and mode are checked on the opened file, so a link swapped in after the check is refused too. - HTTPS only. The token is sent only to the configured Scrape.do API base (default
https://api.scrape.doandhttps://q.scrape.do; override withSDO_BASE_URL/SDO_ASYNC_BASE_URL). A plain-httpbase is refused unless its host islocalhost,127.0.0.1or::1, or you pass--insecure-http— which sends the token in cleartext. Redirects are never followed, so a3xxcannot carry the token elsewhere. - Redaction. The CLI's own messages — errors and the metadata lines on stderr — have the token (literally and percent-encoded) and credential-shaped values (
token=…,api_key=…,"access_token": "…",X-Token: …) replaced with***. Data you asked for — page content (sdo scrape,sdo jobs result), raw plugin bodies, and the JSON output ofsdo plugin,sdo jobs …andsdo account— is written as delivered, except that the token itself (8 characters or longer, literally and percent-encoded, in JSON keys and values alike) is masked: a credential-shaped string that belongs to the data, such as a?token=link on a scraped page, is left alone. - Token hygiene. Surrounding whitespace is trimmed from every token source; a token with whitespace, control or non-ASCII characters inside is refused before any request, with an error naming its source.
- The token is never logged. The only file it is written to is the config file above, and only by
sdo auth login. The only other filessdowrites are the ones you name withsdo scrape --output-file, written with mode0600through a temporary file renamed over the target (an existing file is replaced, not reused with its old mode); a symlink or non-regular file at that path is refused. - Terminal output. Control characters (C0 except newline and tab, DEL, C1, and invalid UTF-8 bytes) in API- or target-supplied text printed to stderr are escaped. A body with NUL or ESC bytes is not written to a terminal unless you pass
--raw.
Treat your token like a password. Rotate it immediately if you believe it has been exposed; rotation is done from your Scrape.do dashboard and invalidates the old token.
sdo mcp serve runs a local MCP server so MCP hosts (editors, agents, assistants) can call Scrape.do through sdo. This introduces a classic confused-deputy risk: the MCP host, not the human, decides when to invoke tools, so the server must never act on its own initiative or on ambient instructions hidden in fetched content.
Design properties that bound this risk:
- Explicit tool calls only. The server performs an action only in response to a structured MCP tool invocation from the connected host. It does not poll or schedule background work.
- Untrusted content is fenced. Scraped pages, plugin output and error text written by the gateway or the target are returned between
--- BEGIN UNTRUSTED CONTENT <nonce> ---and--- END UNTRUSTED CONTENT <nonce> ---lines. The nonce is random per response and is the primary control: a page cannot know it, so it cannot print the real closing line and continue in the server's voice. As a second layer, marker look-alikes inside the content are neutralized — any "untrusted content" phrase and any line shaped like a marker line (three or more dashes, then BEGIN or END) — after folding Unicode tricks away: compatibility forms (NFKC), zero-width and other format characters, combining marks, no-break and other Unicode spaces and dashes, and Cyrillic, Greek and other letters that imitate Latin ones. Server metadata (status, cost when known, URL) is a separate content item, and binary responses (images, PDFs) are described, not returned. This makes the trust boundary visible to the model; it does not make a model immune to instructions in page text. - Bounded output. Each tool result carries at most 128 KiB of outside text; truncation is stated outside the fenced block.
- Token stays server-side. The token is read by the
sdoprocess (same precedence as the CLI) and held only there. It is not a tool argument, and every tool result — success or error — passes through the same redaction as CLI output before it reaches the host, so a gateway or plugin that echoes the token cannot hand it to the model. - Validated input. Tool arguments are checked against each tool's closed input schema before anything is sent: an unknown argument or a mistyped value is a JSON-RPC
-32602error, never silently dropped or coerced. Plugin routes chosen by the model are checked against the documented route shape (no../,?or#), and plugin parameters cannot carry atoken. - Bounded, cancellable work. At most 4 tool calls run at once; a
notifications/cancelledfrom the host abandons the call (and its gateway request) and suppresses its reply. Each call spends Scrape.do credits, and the tool descriptions say so. - Local transport. The server communicates over local stdio with the host process that launched it. It does not open a network listener.
- No privilege escalation. MCP tools expose the same Scrape.do operations the CLI already offers under the same token.
Only connect the bundled MCP server to MCP hosts you trust, and scope the token to the access level the workload actually needs.
sdo collects no telemetry and never checks for updates on its own. There is no analytics, no usage reporting, no crash phone-home, and no background network activity. The only outbound traffic is the API requests you explicitly make to Scrape.do, identified by the User-Agent header sdo-cli/<version> and nothing else (no OS, no runtime version), plus the one exception below. This holds for both the CLI and the bundled MCP server.
The one exception is sdo version --check, and only when you run it: it sends a single anonymous GET https://api.github.com/repos/scrape-do/sdo-cli/releases/latest carrying no token or credential of any kind (the Scrape.do token is not even read) and nothing identifying beyond the same User-Agent header. It does not follow redirects, gives up after 5 seconds, reads at most 64 KiB, and prints only a release tag that matches vMAJOR.MINOR.PATCH, never other text from the response. No automatic, startup, periodic or background check exists, and no setting or environment variable enables one.
-
sdois built on thesdo-goSDK, which is tested against a cross-language conformance corpus shared by every Scrape.do SDK. Development is TDD-first: changes ship with tests, and CI enforces formatting,go vet, the race detector, a coverage floor, secret scanning,govulncheck, OSV scanning and a drift check on the command/MCP surface. -
Dependencies and the SHA-pinned GitHub Actions are kept current by Dependabot, with a 7-day cooldown before a new release is proposed.
-
Releases are built from tagged commits on
mainby GoReleaser, published only after approval in a protectedreleaseenvironment. For version X.Y.Z (the tagvX.Y.Z) a release holds:sdo_X.Y.Z_<os>_<arch>.tar.gzforlinuxanddarwin, andsdo_X.Y.Z_windows_<arch>.zip, each foramd64andarm64(e.g.sdo_X.Y.Z_darwin_arm64.tar.gz). Each archive holds thesdobinary (sdo.exeon Windows),LICENSE,NOTICE,README.md,CHANGELOG.mdandTHIRD_PARTY_LICENSES.txt, the license and notice texts of the Go runtime and every module linked into the binary.<archive>.sbom.json, an SPDX SBOM per archive (e.g.sdo_X.Y.Z_linux_amd64.tar.gz.sbom.json).checksums.txt, the SHA-256 of every file above, andchecksums.txt.cosign.bundle, a cosign keyless signature for it, signed by the release workflow's GitHub OIDC identity and recorded in the Rekor transparency log.
To verify a download (cosign v3):
cosign verify-blob checksums.txt \ --bundle checksums.txt.cosign.bundle \ --certificate-identity "https://github.com/scrape-do/sdo-cli/.github/workflows/release.yml@refs/tags/vX.Y.Z" \ --certificate-oidc-issuer https://token.actions.githubusercontent.com sha256sum --ignore-missing -c checksums.txt -
Sibling repositories under the same governance:
sdo-sdk-python(PyPIsdo-sdk),sdo-sdk-js(npm@scrape-do/sdk), andsdo-go(github.com/scrape-do/sdo-go).
In scope:
- The
sdobinary and its subcommands. - The bundled MCP server (
sdo mcp serve), including token isolation and the confused-deputy properties above. - Token handling and redaction.
Out of scope:
- The Scrape.do API service itself — report those issues to security@scrape.do.
- Vulnerabilities in third-party MCP hosts that connect to the bundled server.
- Misconfiguration on the operator's machine, such as exporting
SDO_TOKENinto an untrusted environment or passing--insecure-httpto a non-local gateway.
This project is licensed under Apache-2.0. Security fixes are released under the same license.