safe-write-mode is a defensive library. It does not:
- Make network calls on its own (you provide the client; the bundled adapters are skeletons)
- Store credentials (you provide them via env)
- Process untrusted input (callers provide validated payloads)
- Run code from configuration
It does:
- Read
process.env.SAFE_WRITE_MODE(and legacyMODE),LOG_LEVEL,WRITE_LOG_DIRfrom environment - Compute SHA-256 hashes of request bodies
- Append to log files in
WRITE_LOG_DIR(default:./logs/) - Write to
process.stderr
If your threat model requires that this library cannot read the environment or write to disk, don't use it as-is. Vendor it, or wrap it.
- Accidental writes from a process running with the default
read-onlymode - Caller-supplied payload typos hitting destructive endpoints (
{ destructive: true }ack required forDELETEand for any path matching/reset|/purge|/wipe|/clear|/flush|/destroy|/factory-reset/i) - SSRF against link-local, loopback, RFC1918, RFC6598, IPv6 ULA, and cloud-metadata endpoints (e.g.
169.254.169.254) - Bearer-token leak via path-injection: example adapters refuse cross-origin paths, protocol-relative paths, scheme-prefixed paths, backslash paths
- Secret leak through structured logs: keys named
authorization|cookie|set-cookie|password|secret|token|api[_-]?key|...are redacted; values matchingBearer …/sk-…/ghp_…/xox?-…/AKIA…are masked - Secret leak through error messages: full URLs (query string + userinfo) are stripped before being interpolated into
HttpErrormessages - Crafted client smuggling:
createIntegrationonly accepts clients constructed viadefineAdapter; the internal write symbol is not exported - Mode tampering:
lockMode()captures the current mode in a frozen closure; later mutations toprocess.env.SAFE_WRITE_MODEare ignored - GraphQL mutation smuggling: classifier strips BOM/Unicode whitespace and string/comment content, refuses mixed-operation documents
- A hostile agent running in the same Node process can:
- Construct its own
defineAdapter(...)client with arbitrary semantics - Set
confirmed: trueanddestructive: trueon its own writes (these flags are caller-controlled — they are ergonomics, not a security boundary) - Modify
process.envbeforelockMode()is called
- Construct its own
- DNS rebind attacks against SSRF: lookup happens at request prep, fetch resolves again at connect time
- Audit-log tamper after the fact: NDJSON files are file-system writable; a hash-chain is on the v0.3 roadmap
To get a real boundary against an in-process agent, combine safe-write-mode with:
- Read-only API tokens at the provider (the strongest mitigation)
- Network-egress allowlists (firewall / VPC)
- A separate process / kernel boundary between the agent and the writes (see
setConfirmationVerifierfor an extension point that can talk to a host process)
The NDJSON audit trail contains:
- ISO timestamps
- Integration names (strings you provide)
- HTTP method + path (strings you provide)
- SHA-256 hashes of bodies (no plaintext)
- Boolean flags (dryRun, destructive)
- Free-form
reasonstrings (caller-provided) - Error messages from caught exceptions (URLs sanitized — see CHAIN-2(d) in audit_swm_v01_external)
It does not contain:
- Request bodies
- Response bodies
- Credentials, tokens, or any header values
Treat audit logs as operationally sensitive — they reveal integration names and call patterns. Directory is created with mode 0700; files are opened with mode 0600. Rotate to long-term storage.
If you find a security issue:
- Do not open a public GitHub issue.
- Use GitHub Private Vulnerability Reporting on https://github.com/maxschottke-spec/safe-write-mode/security/advisories/new — OR email
max.schottke@gmail.comwith subject[safe-write-mode SECURITY]and include a description, proof of concept if possible, and the version you tested against. - Expect an acknowledgement within 5 business days.
Coordinated disclosure preferred. Public credit for reporters who follow the process.
- Vulnerabilities in
node:crypto,node:fs,node:dns, or thefetchruntime - Misuse of the library outside its documented contract (e.g. catching
SafetyErrorsilently to bypass safety; settingSAFE_WRITE_MODE=read-writesystem-wide; never callinglockMode()) - DNS rebind against SSRF guard (acknowledged limitation — pin-IP agent on v0.3 roadmap)
- Audit-log tamper after the fact (hash-chain on v0.3 roadmap)
getMode()readsprocess.env.SAFE_WRITE_MODEat call time unlesslockMode()has been called. Hosts that need a real freeze must calllockMode()during startup before any agent code runs.- Audit log writes are synchronous (
writeSync). High-throughput callers may want to buffer. - HTTP layer follows
redirect: 'manual'and refuses 3xx. If your target intentionally redirects, you must handle it before callingrequest(). - SSRF guard refuses
http://unlessSAFE_WRITE_ALLOW_INSECURE=1is set. The opt-in is intended for local development against a vendor sandbox onlocalhost; the IP-range check still runs.
safe-write-mode is independently audited. See audit_swm_v01_external (internal Max-Schottke memory) for the full history; round-2 fixes (2026-05-20) are documented in test/audit-fixes-r2.test.js as executable regression coverage.