Skip to content

Make the shell sync child process less likely to be flagged by EDR heuristics #2377

Description

@dex4er

Context

In #2376 a user's Microsoft Defender for Endpoint classified Freelens as a
"possible reverse shell" (High, T1059 / T1059.004). The analysis is in
this comment:
the remote address was registry.npmjs.org, the reported socket: on stdout was a
local AF_UNIX socketpair from child_process.spawn, and the most likely trigger was
shell sync, which spawns the user's login shell at every application start.

The current behaviour is benign, so this is not a security fix. It is about how the
spawn looks to an endpoint agent, and about making the alert self-explanatory when
one does fire. Today
compute-unix-shell-environment.injectable.ts
combines several traits that EDR heuristics score individually.

Proposed changes

Three independent changes, each worth doing on its own merits.

1. Pass the command in argv instead of stdin

The command is currently written to the shell's stdin
(#L190),
so the process's command line contains no trace of what is executed. That is both a
pattern EDRs flag on its own and the reason the alert in #2376 was uninformative — the
"Command line" field told the person triaging it nothing, when a visible argv would
have shown at a glance that the shell was dumping environment variables.

There is already precedent in the same function: fish gets the command through
-c
(#L91-L93).

Caveats:

  • zsh/bash read their rc files with -l -i -c '...', but this needs verifying per
    shell rather than assuming.
  • csh-like shells are the reason the stdin path exists — they accept no other options
    alongside -l
    (#L98-L100),
    so they should keep receiving the command on stdin.
  • argv is world-readable through ps. The command contains only a random marker and
    a binary path, no secrets — the environment itself still travels over the pipe — but
    this should be a conscious decision rather than an oversight.
  • The leading space that keeps the command out of shell history
    (#L97)
    becomes unnecessary for shells switched to -c.

2. Dump the environment without an interpreter inside the interpreter

The login shell currently executes the Electron binary as Node with inline code to
serialize process.env as JSON
(#L88,
ELECTRON_RUN_AS_NODE=1 at
#L107).
A GUI application's own binary being re-executed as a script interpreter from a shell
is probably the single most alarming trait in the whole sequence, and it exists only
to turn a map of strings into JSON.

env -0 (or export -p) does the same job with no interpreter at all: NUL-delimited
KEY=VALUE records, still wrapped in the existing random marker so rc-file noise
stays separable.

Caveats:

  • parsing moves from JSON.parse to splitting on NUL and on the first =; values
    containing newlines are handled correctly by env -0, which is precisely why the
    NUL variant is preferable to plain env,
  • shell functions exported into the environment (BASH_FUNC_*) and non-UTF-8 values
    need a test,
  • the Windows/PowerShell branch
    (#L78-L86)
    is out of scope — computeShellEnvironment is a no-op on Windows anyway.

3. Re-examine detached: true

The shell is spawned detached
(#L119-L123),
which makes it a process group leader. Cancellation already goes through an
AbortSignal, so it is worth checking whether the flag is still needed — and if it
is, saying why in a comment. An unexplained detached on a spawned shell is one more
thing an endpoint agent scores and a reader has to guess about.

Explicitly not claimed

None of this can be validated against Defender for Endpoint on our side: the
maintainers run the consumer edition of Microsoft Defender, which does not implement
these behavioural detections. So no change here should be described as "silencing the
scanner". Each is justified independently — a visible argv aids triage, one fewer
interpreter in the chain is simpler, and an unexplained spawn flag is worth resolving
either way.

This is also independent of the preferences discussed in #2376 (an opt-out for the
startup version check and for shell sync), which are deferred until after v2.0.0.

Acceptance criteria

  • compute-unix-shell-environment.test.ts passes, with cases covering zsh, bash, fish
    and a csh-like shell.
  • Environments containing multi-line values and exported shell functions round-trip
    correctly.
  • No ELECTRON_RUN_AS_NODE in the shell sync spawn path.
  • For shells switched to -c, the command is visible in ps output.
  • Shell sync still populates PATH such that kubectl and kubeconfig exec
    credential plugins resolve when the app is launched from Finder or the Dock.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions