actor-ts is a distributed actor framework: it opens sockets, parses
wire frames, deserializes untrusted payloads and persists application
state. Bugs in any of that are security bugs. This page says how to
report one, what the project treats as in scope, and what the release
pipeline does to keep the shipped closure honest.
The project is pre-1.0 and there are no maintenance branches. Fixes
land on develop and ship in the next release; nothing is backported.
| Version | Supported |
|---|---|
Latest published 0.x.y |
✅ Yes |
Any earlier 0.x.y |
❌ No — upgrade to the latest release |
If you are pinned to an older version and cannot upgrade, say so in the report: it does not change what gets fixed, but it does change what mitigation advice is useful to you. See the version policy for what "pre-1.0" implies about breaking changes.
Do not open a public issue for anything that is exploitable against a deployed system.
Use GitHub's private vulnerability reporting: the repository's Security tab → Report a vulnerability. That opens a draft advisory visible only to you and the maintainer, and it is the channel that produces a CVE and a published advisory at the end.
If that button is not there, private reporting has not been enabled on the repository yet. In that case open a normal issue with the
[Security]prefix that says only "I have a finding in<subsystem>and need a private channel" — no reproduction, no payload, no affected version — and wait for a reply before sending details. An empty placeholder issue discloses nothing.
Please include, once you are on a private channel:
- the affected version (or commit) and the runtime (Bun / Node / Deno),
- a reproduction — a failing test or a short script is worth more than prose,
- the impact you believe it has, and under which of the deployment shapes below,
- any mitigation you already found.
I will respond as quickly as I can. This is a single-maintainer project: there is no security team and no paid SLA, so no fixed window is promised here rather than promised and missed. Reports are read, and a serious one is worked before feature work. If yours has gone quiet longer than feels right, ping the tracker with a content-free nudge — that is a reasonable thing to do, not a rude one.
Not everything needs the private path. Defense-in-depth items, audit
follow-ups, hardening suggestions and anything already public go
straight into the tracker with the
security template,
which applies the security label and a severity: label. That is how
the existing audit catalogue was filed, and it is the preferred route
when you are unsure — a public issue that should have been private can
be converted; a private report costs a round trip.
The framework ships insecure-by-default transports on purpose, with the secure configuration documented next to them. That makes the scope boundary specific rather than obvious, so it is written out here.
- The cluster transport defaults to plain TCP with no peer authentication. This is a deliberate default for a private network, documented in Cluster security. "I ran a cluster on a hostile network without TLS and a stranger joined it" is the documented behaviour, not a vulnerability.
- Anything requiring an attacker who already has code execution in the actor system's own process, or read access to its configuration and key material.
- Denial of service by an operator against their own system — an unbounded mailbox that a local caller fills, a dispatcher starved by application code.
- Vulnerabilities in the example applications under
examples/, which are illustrations and are not published to npm. Report them, but as ordinary bugs.
- A documented mitigation that does not deliver what it says. If TLS is configured as the docs describe and peers still are not authenticated, that is a vulnerability, and one that has happened here before.
- Anything reachable from untrusted input on a socket: wire-frame parsing, HTTP routing and middleware, WebSocket framing, broker payloads, deserialization.
- Missing or bypassable bounds on untrusted input — frame-size limits, header limits, decompression ratios.
- Cryptographic mistakes: predictable identifiers where unpredictability is load-bearing, key handling, at-rest encryption and key rotation.
- Persistence: reading or writing another entity's stream, replay of a fenced writer, injection through a persistence backend.
- Anything that lets a cluster member escalate beyond what membership is supposed to grant.
These run in CI on every change, and are what a downstream consumer is relying on when they install a published version:
| Gate | Where | What it catches |
|---|---|---|
CodeQL (javascript-typescript) |
.github/workflows/codeql.yml — pull requests, pushes to main/develop, weekly |
Injection, path traversal, unsafe deserialization and the rest of the security-extended query suite. Results land in the repository's code-scanning alerts. |
bun audit |
.github/workflows/package-health.yml — same triggers plus a weekly cron |
Published advisories against the versions bun.lock actually pins. |
| SHA-pinned actions | tests/unit/ci/WorkflowHygiene.test.ts |
A mutable action tag being repointed under a job that holds a publishing credential. |
| npm provenance | .github/workflows/publish.yml |
Ties the published tarball to the workflow run and commit that built it. |
| CycloneDX SBOM | .github/workflows/publish.yml |
Attached to every GitHub Release, so a consumer can diff the closure without reconstructing it. |
bun audit reads bun.lock. GitHub's dependency graph does not resolve
that file — it records the unresolved ranges from package.json — which
is why Dependabot's alerts on this repository have only ever come from
the npm lockfiles under examples/, and why actions/dependency-review-action
is deliberately not used here.
The audit gate fails on any high or critical advisory, and there are
no accepted ones: the lint:audit script carries no --ignore flag.
It used to. The gate landed with a baseline of eleven suppressed
advisory IDs — every high advisory the lockfile carried at the time —
so that it could land green rather than land red and be ignored, with
#779 tracking the
refresh that would remove them. Six more were published upstream
afterwards, all against fast-uri and all against a lockfile nobody
had touched; those were never suppressed, and the gate did what a gate
is for and went red.
The refresh has now landed and clears both halves. All nineteen high
advisories in the closure turned out to be reachable by an in-range
bump:
@fastify/static, brace-expansion, fast-uri (both major lines),
find-my-way and ws all had a fixed release inside the range
package.json already declared. So only bun.lock moved. No
dependency range was widened, no overrides entry was added, and a
consumer installing actor-ts resolves the fixed versions from the
published manifest exactly as this repository does.
If a suppression is ever needed again it belongs back here as a table —
advisory, package, severity, and the path it is reached through — and
adding one is a deliberate act, not a way to get a change through.
tests/unit/ci/SecurityPolicy.test.ts asserts that this section and the
lint:audit ignore list name the same set of advisories, so one cannot
be silenced without appearing here, and a row cannot linger here after
its suppression is dropped.