You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Threat model: closed-group cluster where actor names are derived from user input (entity IDs, tenant IDs, user-supplied names). Risk is log injection — actor names land in nearly every log line via system.log.withSource(this.path.toString()).
Affected files
src/ActorPath.ts:5-15 — ActorPath constructor accepts any string name; no validation.
src/internal/ActorCell.ts:99-109 — ActorCell constructor concatenates the name into path then sets this.log = system.log.withSource(this.path.toString()). Every log line from this actor now contains the name.
src/internal/ActorCell.ts:123-134 — actorOf accepts any name?: string and registers it as _children key.
src/Logger.ts:42-45 — log emission merges the source string (which contains the name) into every record without escaping.
Relationship to #126 (A6.2 — ActorPath path-traversal)
This issue and #126 address the same code site (ActorPath/ActorCell name validation) from two angles:
The implementation in #126 already covers \0, \r, \n (in its validator). If #126 is implemented first, this issue is largely resolved as a side-effect. It's kept as a separate issue because:
The threat model is different (log integrity vs route bypass), and someone reviewing only "log injection" findings should find it.
The fix design here adds a narrower scope (control chars only) for deployments that do want / in actor names (e.g. legacy compat) but still need log integrity.
system.log.withSource(this.path.toString()) — every log line from this actor includes the path string in its source field.
this._children map key in ActorCell — used in error messages, debug output.
Logger record fields (level, time, source, msg, ctx).
Every common logger (the built-in Pretty/JSON formatters, pino, winston) emits records as newline-delimited entries. A newline embedded in the source name produces a fake log line:
The log aggregator (Loki/ELK/Splunk) sees two records — the second one being the attacker's injected line, parseable as a fresh entry.
Same vulnerability shape as CVE-2018-1000805 (Python's logging module), CVE-2021-44228 (Log4j's surface area was larger but newline-injection was one vector), countless logging-related CVEs across all stacks. The OWASP class is A09 (Security Logging Failures).
Exploit walkthrough
Step 1 — App spawns actors per user-supplied identifier:
Step 2 — Attacker callsPOST /session?userId=victim%0AERROR%20user%20admin%20deleted%20by%20attacker%20-%20remember%20to%20investigate. URL-decoded: userId = 'victim\nERROR user admin deleted by attacker - remember to investigate'.
Step 3 — Actor is spawned, name becomes 'session-victim\nERROR user admin deleted by attacker - remember to investigate'. First log line from the actor:
{"level":"debug","time":"...","source":"actor-ts://default/user/session-victim
ERROR user admin deleted by attacker - remember to investigate","msg":"actor started"}
Event 2: ERROR user admin deleted by attacker - remember to investigate", "msg":"actor started"} — parses as a malformed JSON line, may be promoted to ERROR severity by some pipelines, alerts a SOC analyst.
Step 5 — SOC analyst investigates the injected ERROR line. Cost: 30-60 minutes of analyst time per injection. At scale (thousands of injections), SOC drowns in noise; real alerts get missed.
Realistic worst case:alert fatigue + audit trail forensic confusion. Combined with #133 (pid log injection) and #126 (path traversal), this is the third site in the same family.
How the 8 already-landed security fixes inform this
Memcached CRLF guard — exact same shape: reject \r/\n in keys at the wrapper. Drop in the same check here.
FS path-traversal (assertSafeKey) — same boundary-validation pattern.
Hello-handshake hijack defence — validated identity at the connection boundary; rejected mismatched values. Same shape: reject malformed names at construction.
The shape: the constructor of an addressable type is the one place to validate; trying to escape at log-emission time is hopeless because not every log path goes through framework-controlled code.
Fix design
Track 1 — assertSafeActorName validator in ActorPath constructor (primary). If #126 ships first, this is already done. If standalone, the narrower scope is:
functionassertLogSafeName(name: string): void{if(typeofname!=='string'||name.length===0){thrownewError('ActorPath: name must be a non-empty string');}// Control chars: NUL, CR, LF, vertical tab, form feed, backspace, etc.if(/[\x00-\x1f\x7f]/.test(name)){thrownewError('ActorPath: name contains control characters');}}
Apply in ActorPath constructor when parent !== null (root paths excepted).
Track 2 — Defence-in-depth at ActorCell.actorOf. Re-validate the childName (which may have been auto-generated as $<counter>) before adding to _children.
Track 3 — Log-source sanitiser as fallback. Even if name validation passes, the log emitter can replace control chars with � (Unicode replacement) before emission. This catches the case where:
A name is constructed legitimately but contains a Unicode char that renders as a newline in some terminals (rare).
A future code path bypasses ActorPath constructor (unlikely but possible).
This is belt-and-braces; the constructor check is the primary defence.
Track 4 — Documentation. README "Known security caveats" gets a bullet (consolidate with #126 if both ship):
- Actor names + persistence ids: framework rejects control chars
(\0, \r, \n, etc.) at construction. Apps deriving names from
user input still need their own allow-list for character class
(we only enforce log-safety, not URL-safety or DB-safety).
API surface
No new public API. Behaviour change: system.actorOf(props, 'foo\nbar') throws.
If implemented via assertLogSafeName alone (standalone of #126), names with / continue to work (the routing-correctness angle is separate).
If implemented via #126's assertSafeActorSegment, names with / are also rejected; this is the stricter / preferred path.
Backward compatibility
Breaking for any code that puts control chars in actor names. Audit:
$ grep -rE "actorOf\([^,]+,\s*['\"][^'\"]*[\\\\\\x00-\\x1f][^'\"]*['\"]\)" tests/ examples/
(no matches)
Document in CHANGELOG under "Security defaults: actor name validation". Note relationship to #126 (single coordinated change, two issue closures).
Test plan
Newline rejection — actorOf(props, 'foo\nbar') throws with clear message.
CRLF rejection — 'foo\r\nbar' throws.
NUL rejection — 'foo\0bar' throws.
All control chars rejected — parameterised test over \x00..\x1f and \x7f.
Log-source sanitiser test — directly call the sanitiser with a control-char string, verify replacement.
Log-output integration test — spawn an actor with a "legitimate" name, capture log output, verify it's parsable as exactly N JSON records (not more — proves no injection).
Happy path — 'foo', 'foo-bar', 'session-123', '$1' all work.
Regression — full 1 720+ tests pass.
Acceptance criteria
ActorPath constructor rejects control chars in name (when parent !== null).
ActorCell.actorOf re-validates childName before registration.
(Optional) Logger source-field sanitiser replaces control chars with �.
Severity / Size
system.log.withSource(this.path.toString()).Affected files
src/ActorPath.ts:5-15—ActorPathconstructor accepts any stringname; no validation.src/internal/ActorCell.ts:99-109—ActorCellconstructor concatenates the name intopaththen setsthis.log = system.log.withSource(this.path.toString()). Every log line from this actor now contains the name.src/internal/ActorCell.ts:123-134—actorOfaccepts anyname?: stringand registers it as_childrenkey.src/Logger.ts:42-45— log emission merges the source string (which contains the name) into every record without escaping.Relationship to #126 (A6.2 — ActorPath path-traversal)
This issue and #126 address the same code site (
ActorPath/ActorCellname validation) from two angles:/,\,..,., scheme prefixes — the routing-correctness angle.\r,\n,\0, and other control characters — the observability-integrity angle.The implementation in #126 already covers
\0,\r,\n(in its validator). If #126 is implemented first, this issue is largely resolved as a side-effect. It's kept as a separate issue because:/in actor names (e.g. legacy compat) but still need log integrity.If #126 ships first, close this as "fixed by #126" and add the log-injection test cases there.
Background
Actor names flow into:
ActorPath.toString()→actor-ts://default/user/<name>/...system.log.withSource(this.path.toString())— every log line from this actor includes the path string in itssourcefield.this._childrenmap key inActorCell— used in error messages, debug output.Every common logger (the built-in Pretty/JSON formatters, pino, winston) emits records as newline-delimited entries. A newline embedded in the source name produces a fake log line:
The log aggregator (Loki/ELK/Splunk) sees two records — the second one being the attacker's injected line, parseable as a fresh entry.
Same vulnerability shape as CVE-2018-1000805 (Python's logging module), CVE-2021-44228 (Log4j's surface area was larger but newline-injection was one vector), countless logging-related CVEs across all stacks. The OWASP class is A09 (Security Logging Failures).
Exploit walkthrough
Step 1 — App spawns actors per user-supplied identifier:
Step 2 — Attacker calls
POST /session?userId=victim%0AERROR%20user%20admin%20deleted%20by%20attacker%20-%20remember%20to%20investigate. URL-decoded:userId = 'victim\nERROR user admin deleted by attacker - remember to investigate'.Step 3 — Actor is spawned, name becomes
'session-victim\nERROR user admin deleted by attacker - remember to investigate'. First log line from the actor:Step 4 — Log aggregator parses as two events:
{"level":"debug", ..., "source":"actor-ts://default/user/session-victim", ...}(truncated)ERROR user admin deleted by attacker - remember to investigate", "msg":"actor started"}— parses as a malformed JSON line, may be promoted to ERROR severity by some pipelines, alerts a SOC analyst.Step 5 — SOC analyst investigates the injected ERROR line. Cost: 30-60 minutes of analyst time per injection. At scale (thousands of injections), SOC drowns in noise; real alerts get missed.
Realistic worst case: alert fatigue + audit trail forensic confusion. Combined with #133 (pid log injection) and #126 (path traversal), this is the third site in the same family.
How the 8 already-landed security fixes inform this
\r/\nin keys at the wrapper. Drop in the same check here.assertSafeKey) — same boundary-validation pattern.The shape: the constructor of an addressable type is the one place to validate; trying to escape at log-emission time is hopeless because not every log path goes through framework-controlled code.
Fix design
Track 1 —
assertSafeActorNamevalidator inActorPathconstructor (primary). If #126 ships first, this is already done. If standalone, the narrower scope is:Apply in
ActorPathconstructor whenparent !== null(root paths excepted).Track 2 — Defence-in-depth at
ActorCell.actorOf. Re-validate thechildName(which may have been auto-generated as$<counter>) before adding to_children.Track 3 — Log-source sanitiser as fallback. Even if name validation passes, the log emitter can replace control chars with
�(Unicode replacement) before emission. This catches the case where:ActorPathconstructor (unlikely but possible).In
Logger.ts's emit path:This is belt-and-braces; the constructor check is the primary defence.
Track 4 — Documentation. README "Known security caveats" gets a bullet (consolidate with #126 if both ship):
API surface
No new public API. Behaviour change:
system.actorOf(props, 'foo\nbar')throws.If implemented via
assertLogSafeNamealone (standalone of #126), names with/continue to work (the routing-correctness angle is separate).If implemented via #126's
assertSafeActorSegment, names with/are also rejected; this is the stricter / preferred path.Backward compatibility
Breaking for any code that puts control chars in actor names. Audit:
Document in CHANGELOG under "Security defaults: actor name validation". Note relationship to #126 (single coordinated change, two issue closures).
Test plan
actorOf(props, 'foo\nbar')throws with clear message.'foo\r\nbar'throws.'foo\0bar'throws.\x00..\x1fand\x7f.'foo','foo-bar','session-123','$1'all work.Acceptance criteria
ActorPathconstructor rejects control chars inname(whenparent !== null).ActorCell.actorOfre-validateschildNamebefore registration.Loggersource-field sanitiser replaces control chars with�.