This section documents the project's security requirements: guarantees the maintainer commits to, and limits that callers must account for.
- Authenticated transport: every FaxDrop API call goes over HTTPS with
the user-supplied
X-API-Key. No fallback to HTTP, no key in URL params. - Input validation: all tool inputs are validated by Zod schemas before
reaching
FaxDropClient. The fax recipient number must match E.164 (/^\+[1-9]\d{6,14}$/); thefaxIdis URL-encoded; the uploadfilePathmust be absolute and the file must have an allowed extension (PDF, DOCX, JPEG, PNG) and be ≤10 MB — all enforced before any network call. - No secret leakage:
FaxDropError.toString()andtoJSON()never include the raw API response body. The audit log redactsapiKey,authorization,password,token,secret,x-api-keyat any depth (seeredactSensitiveinsrc/middleware.ts, covered by property-based tests intest/fuzz.test.ts). - Supply-chain integrity: every release artifact is signed with Sigstore
(
*.sigstore) and ships an SLSA in-toto attestation (*.intoto.jsonl). npm publishes carry provenance. All GitHub Actions in.github/workflows/are pinned by full commit SHA. - Least-privilege CI: the release workflow is split into a read-only
build job and a publish job (release-only) that holds
NPM_TOKEN. - Defense against runaway agents: dry-run mode (
FAXDROP_MCP_DRY_RUN=true) exercises a write tool without actually sending a fax. FaxDrop itself enforces per-API-key rate limits (per-minute / per-hour / per-day) and returns429withretry_afteron excess; the MCP surfaces this to the caller aserror_type: "rate_limited". See FaxDrop's API docs for the current numbers. - Optional audit trail:
FAXDROP_MCP_AUDIT_LOG=/abs/path/audit.logwrites an append-only JSON Lines record (file mode0o600, sensitive fields redacted) of every write call. Paths under POSIX system roots (/etc,/usr,/bin,/sbin,/sys,/proc,/boot,/dev) are rejected with a clear error — those locations should never receive an MCP audit log, even if the launching process happens to have write permission. Use a path under$HOMEor another writable user-owned directory. - HTTPS-only API base URL:
FAXDROP_API_BASE_URLis validated at server startup. Non-HTTPS schemes (http, file, data, ftp, …) and any host in the loopback / RFC 1918 / link-local / cloud-metadata / IPv6 ULA /.localhostnamespace are rejected — fail-fast at startup rather than silently routing the bearer API key + every fax payload to a cleartext or attacker-controlled endpoint. - POSIX-only platform: faxdrop-mcp requires
fs.constants.O_NOFOLLOW(the symlink TOCTOU barrier betweenrealpathandopen). Windows does not exposeO_NOFOLLOW, so the server refuses to start on Windows with a clear error. Use WSL or another POSIX environment. - Fail closed: 60 s
AbortSignal.timeouton every fetch; missingFAXDROP_API_KEYexits at startup. - Outbox jail: every uploaded file must live inside
FAXDROP_MCP_WORK_DIR(default~/FaxOutbox/, auto-created mode0o700). Any path outside the outbox is rejected afterrealpathcanonicalization, preventing accidental or agent-driven exfiltration of~/.ssh/,~/Library/Keychains/, or any other sensitive location. - Symlink hardening on
filePath: leaf symlinks are rejected atlstat(the actual attack vector —safe.pdf → /etc/passwd); the canonical path is resolved viarealpath; the open passesO_NOFOLLOWas a TOCTOU barrier in case a leaf symlink sneaks in between the lstat and the open. The server refuses to start on platforms whereO_NOFOLLOWis undefined (Windows) so the TOCTOU guard never silently degrades. - File-content magic-byte verification: after the chunked read, the
first bytes of every uploaded file are matched against a per-extension
signature table (
%PDF-for.pdf,PK\x03\x04for.docx,FFD8FFfor.jpeg/.jpg,89504E47for.png). Catches both an attacker-placed misnaming (id_rsa→id_rsa.pdfto sneak a binary through the outbox jail) AND operator typos (a.docxthat is actually a legacy.docbinary). - 3-layer phone-number gate on
recipientNumber(default modepairing— HITL approve-by-default): TYPE → COUNTRY → per-number policy. Layers 1+2 are immutable at runtime — no per-call approval can bypass them. Backed bylibphonenumber-js/maxfor accurate type classification. - Output sanitization: every tool response text is stripped of
ASCII/Unicode control characters and zero-width formatters (BiDi
overrides, ZWSP, ZWJ, BOM…) and wrapped in
<untrusted-tool-output>…</untrusted-tool-output>fences. The fence closing tag itself is escaped if it appears inside the body, so a crafted FaxDrop response can't break out. - Discard non-JSON FaxDrop responses: a non-JSON body (HTML 5xx
page, proxy interception) is rejected with
error_type: "invalid_response", body discarded — never forwarded to the LLM.
Note on
structuredContent: every tool response carries both a sanitized + fencedcontent[0].text(safe for direct LLM display) AND a rawstructuredContentfield (the parsed JSON, for programmatic consumers). The raw field is not sanitized or fenced — re-injectingstructuredContent.messagedirectly into a downstream LLM prompt would bypass the fence. Usecontent[0].textfor display; treatstructuredContentas untrusted data.
- Compromise of the host environment: if your shell, terminal, or MCP
client is compromised, your
FAXDROP_API_KEYand the documents you have on disk can be stolen by the attacker. This MCP cannot detect or prevent that. - Malicious LLM prompts (prompt injection): an LLM that exposes
faxdrop_send_faxto untrusted content (an email, a fetched web page) can be tricked into sending an arbitrary file to an attacker-controlled number. The tool description requires user confirmation, but enforcement is up to the MCP client. Mitigations: enableFAXDROP_MCP_DRY_RUN, require human-in-the-loop confirmation, or do not expose this MCP to channels carrying untrusted content. - Prompt injection through fax response data: FaxDrop returns the
recipientNumber, status messages, and any error body as text fields in the tool response. If a malicious user has previously caused a fax to enter your account (e.g. via a number they own), instructions placed in those fields reach the LLM. More importantly, the cover-page fields you submit (coverNote,recipientName,subject,senderCompany,senderPhone) round-trip throughfaxdrop_get_fax_statusin some response shapes — content originally drafted by an upstream agent can re-enter the LLM context as "trusted" tool output.content[0].textis sanitized + fenced; never auto-execute a follow-upfaxdrop_send_faxbased on fields read from a status response without explicit user confirmation. - Account-level FaxDrop security: 2FA, billing, fraud detection, key rotation are FaxDrop's responsibility, not this MCP's.
- Network-level attackers beyond what TLS provides: this MCP relies on
Node's built-in
fetchand the system trust store. No certificate pinning. - Logging downstream of this MCP: the audit log redacts sensitive fields, but if the MCP client (Claude Desktop, Cursor, etc.) records tool inputs to its own log, that is outside this project's control.
Every published release of faxdrop-mcp is cryptographically signed.
There is no private signing key to manage: signing is keyless via
Sigstore using GitHub's OIDC identity
through the actions/attest-build-provenance
workflow. The trust chain is: GitHub OIDC → Fulcio (short-lived cert) →
Rekor (transparency log).
Three independent ways to verify:
npm view faxdrop-mcp@<version> --json | jq .dist.attestations
npm install --ignore-scripts faxdrop-mcp@<version>
# or, for the strict provenance check across the dependency tree:
npm audit signaturesgh release download v<version> --repo klodr/faxdrop-mcp --pattern 'index.js*'
gh attestation verify index.js --repo klodr/faxdrop-mcpThe index.js.sigstore bundle is what actions/attest-build-provenance
emits: a Sigstore-format bundle containing the DSSE-wrapped SLSA in-toto
attestation plus the Fulcio certificate and the Rekor inclusion proof.
That's the file cosign wants for keyless verification:
cosign verify-blob-attestation \
--bundle index.js.sigstore \
--certificate-identity-regexp '^https://github\.com/klodr/faxdrop-mcp/' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
index.jsThe companion index.js.intoto.jsonl shipped in the same release is the
DSSE envelope on its own, exposed for tools (like OpenSSF Scorecard's
Signed-Releases check) that scan release assets by file extension.
Any verification failure means the artifact was not built by the official release pipeline — do not install it.
Every GitHub Release ships two SBOMs generated from the runtime dependency tree (devDependencies pruned before syft walks the tree) by anchore/sbom-action:
sbom.spdx.json— SPDX 2.3 JSONsbom.cdx.json— CycloneDX 1.6 JSON
Each SBOM carries its own Sigstore attestation binding it to the dist/index.js of the same release run. The attestation subject is the artifact (dist/index.js), not the SBOM file itself — gh attestation verify therefore expects the artifact path plus an explicit --predicate-type selecting which SBOM flavor to check:
# Download the release artifact + SBOMs first
gh release download v<version> --repo klodr/faxdrop-mcp \
--pattern 'index.js' --pattern 'sbom.*.json'
# SPDX
gh attestation verify index.js --repo klodr/faxdrop-mcp \
--predicate-type https://spdx.dev/Document/v2.3
# CycloneDX
gh attestation verify index.js --repo klodr/faxdrop-mcp \
--predicate-type https://cyclonedx.org/bomThen feed the SBOMs into grype, trivy, dependency-track, or any SPDX/CDX-aware scanner.
@modelcontextprotocol/sdk (the MCP SDK) carries a transitive HTTP/SSE/OAuth surface — express, hono, jose, ajv, cors, cookie-signature, pkce-challenge, eventsource, eventsource-parser, raw-body, express-rate-limit — to support the SDK's other transports (Streamable HTTP, SSE, OAuth flows). faxdrop-mcp only ever wires StdioServerTransport, so none of those packages are reachable from the runtime entrypoint.
What this means in practice:
- In
node_modules/afternpm install: yes, those packages are present (they are standard transitive dependencies —npmandpnpmboth materialise them). - In the published
dist/index.js: no.tsupperforms tree-shaking at build time; the bundled artifact is ~34 KB and contains only the stdio code path. - In the runtime address space at execution: no.
dist/index.jsonlyimports the stdio bits;express/hono/joseetc. are never loaded bynode.
The supply-chain concern that remains is the install-time risk: a malicious update to any of those transitive packages could land in node_modules/ and run a postinstall script. Mitigations in place:
socket.yml—unstableOwnership,unmaintained, andmanifestConfusionrules enabled (sinceklodr/faxdrop-mcpv0.4.0). These fire on transitive owner changes / abandonware / manifest mismatch — exactly the supply-chain attack surface that hitevent-stream,ua-parser-js,nx.- Dependabot security updates on every PR.
- OSSF Scorecard
Pinned-Dependenciescheck on every push. npm auditreports 0 vulnerabilities across the current 348-package transitive tree.- Upstream MCP SDK is tracking a stdio-only factor in modelcontextprotocol/typescript-sdk#1924; when that ships, the transitive HTTP surface will collapse to zero installed packages.
If a vulnerability is reported in any of those transitive deps, the runtime impact on faxdrop-mcp is bounded by tree-shaking (the code never runs), but npm audit and Dependabot will still surface the advisory and the maintainer will pin a non-vulnerable version on the next release.
If you discover a security vulnerability in faxdrop-mcp, please report it privately so we can address it before any disclosure.
Use GitHub's Private vulnerability reporting feature. Maintainers will receive your report directly.
If for any reason you cannot use GitHub's private reporting, open an issue with only the message "private security report — please contact me" and a maintainer will reach out.
Do not open a public issue with vulnerability details before a fix is released.
- A clear description of the issue
- Steps to reproduce (proof of concept if possible)
- Affected versions
- Suggested mitigation if you have one
- Acknowledgement: within 72 hours
- Initial assessment: within 7 days
- Fix or mitigation: depends on severity, typically within 30 days for high/critical issues
This policy covers vulnerabilities in this repository's code (the MCP server itself). Issues in upstream dependencies should be reported to those projects directly; we will track the CVE and update our pinned versions.
- Never commit your
FAXDROP_API_KEYto version control. Use environment variables or your MCP client's secret management. - The
faxdrop_send_faxtool reads files from the user's local filesystem — only expose this MCP to agents you trust to act on your behalf, or run withFAXDROP_MCP_DRY_RUN=trueto test prompts safely. - Be aware that exposing
faxdrop_send_faxto an LLM that processes untrusted content opens a prompt injection vector (e.g. an email asking the agent to fax it elsewhere). Use human-in-the-loop confirmation in your client. - Keep this package updated; vulnerable versions may trigger Dependabot alerts on projects that depend on it, provided Dependabot security updates are enabled for the consuming repository.
Thanks for helping keep faxdrop-mcp and its users safe.