MVF-2026-0010 — the editor extension's release task passed the password in the argument list
The VS Code extension's task generator put a task's password property on the command line of the Mutant process it spawned, as --password <value>, where any process running as the same user could…
| Identifier | MVF-2026-0010 | | Internal row | M26-LSP-017 | | Severity | S3, priority P1 | | Affected | extension 0.1.0 and 0.2.0 (present in the 2.4.0 and 2.5.0 language trees) | | Not affected | extension 0.0.1 – 0.0.6; language releases 1.0.0 – 2.3.0. No CLI or language release is affected. | | Fixed in | extension 0.3.0, released with Mutant 2.6.0 | | Reported | internally; Stage B2 audit, confirmed 2026-09-25 | | Published | 2026-10-05 | | CVE / GHSA | none assigned | | CVSS:3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N | | CVSS:4.0 | CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N |
Summary#
The VS Code extension's task generator put a task's password property on the
command line of the Mutant process it spawned, as --password <value>, where any
process running as the same user could read it from the process table for the life
of the build. The flag it chose was already the deprecated, argv-visible spelling
at 2.5.0, where the CLI warns on it and says it will require an explicit opt-in in
the next minor release — so the extension was wiring operators into the one
password input the language had announced it would stop accepting.
The two version numbers, because the extension does not version by the language#
This row is the one place in this register where "which version am I on" has two answers, and the register's old one-word entry for it ("extension only") was not a range and overstated the extension half. Checked by source:
| affected | |
|---|---|
| extension versions — what an operator installs | 0.1.0 and 0.2.0 |
| language trees that contained that source | 2.4.0 and 2.5.0 |
The boundaries are exact rather than inferred. The string "--password" changed in
only two commits in the extension's whole history: the one that added it, which
is also the commit that bumped the version from 0.0.6 to 0.1.0, so the flag and
0.1.0 arrived together; and the one that removed it. A later commit bumped 0.1.0 to
0.2.0 with the flag still in place. So every commit between the two carried it, and
the version list stands however a .vsix was packaged.
The fixed extension version is 0.3.0, released with Mutant 2.6.0. The fix
itself did not bump the version: until that release the extension's own changelog
filed the entry under ## Unreleased above the ## 0.2.0 heading. So 0.2.0 is an
affected version, and an operator who stays on it still has this.
Whether 0.1.0 or 0.2.0 was ever published to a marketplace is not recorded in git, and this page does not assert it. What git does show is a publisher name, a packaging tool, a prepublish script, a per-platform packaging script, and "New this release:" written under both version headings.
Impact#
A credential the operator wrote into a task definition became an argument of the spawned process, readable from the process table by anything running as the same user, for as long as the build ran.
One sentence in this project's own 2.6.0 changelog entry is wrong about this and
is corrected rather than repeated here. It says nothing reached the disk or a
log, and that the exposure was the argument list alone. That is true of what the
tool wrote and false of what the route required: the password property is part of
a task definition, which lives in .vscode/tasks.json in the workspace — a file on
disk, and commonly one committed alongside the case scripts. The credential had to
be written there to be used at all. The internal row says so in its own words; the
changelog sentence lost it.
Nothing here was executed. The internal finding is static, with the task-argument generator extracted and run on its own to show the argument list it emits. What is established is that a credential the operator chose to write into a config file was exposed to local users on the same machine. No attacker gain, no decryption of any particular artefact and no remote vector is established, and the internal row itself records that this is a user-configured credential rather than a verification bypass.
What must not be read into the fix#
Upgrading does not take the password off the command line for the configuration
that was affected. After the fix, a task that keeps its password property emits
--password-insecure <value> — still on argv, still readable from the process
table, for the life of the build. The fix renames the flag to the CLI's documented
explicit opt-in so that 2.6.0 will accept it, adds an alternative that keeps the
secret off argv, and documents the exposure. It does not end it.
Only setting passwordFile instead, or leaving both unset and being prompted in
the terminal, keeps the secret off the argument list.
Details#
At 2.4.0 and 2.5.0 the generator handled the credential inside the release arm of
its command switch: if the task definition had a password, it pushed
--password and the value. The value came straight from a password property that
the task schema declared as a plain string described as an optional
encryption/decryption password, and the resulting argument array was handed to a
shell execution of the Mutant CLI.
The wrong decision was the flag, not the fact of passing one. At 2.5.0 the CLI
already warned on bare --password and already offered --password-file,
--password-stdin and --password-insecure on both the release and generate
commands.
The fix#
The credential handling moved out of the release arm into the generator's common
tail, so it covers generate, run and release alike. The existing password
property now emits --password-insecure, the CLI's explicit opt-in. A new
passwordFile property emits --password-file, which keeps the secret off argv.
The schema description now says which is which, in plain terms: the password
route is visible in the process table to every local user, prefer passwordFile,
or leave both unset to be prompted.
It deliberately does not pick a winner when both are set: both flags are emitted and the CLI refuses with "more than one password source given", so the conflict is reported rather than silently resolved. The task-argument generator was also exported for testing, which is the structural change that lets a test assert the generated argument list instead of reasoning about it.
Fixture#
This row's own fixture is the extension's test,
mutant-vscode-extension/src/test/suite/extension.test.ts — the test asserting
that a generated task never passes the refused flag. For each of the generate, run
and release commands with both properties set, it asserts the argument list
contains neither --password nor its short spelling and ends with the
--password-file and --password-insecure pairs; and that a task with no password
field yields the source file alone, so the CLI prompts.
The CLI-side refusal this depends on is held by
credential/credential_test.go (TestBarePasswordIsRefusedNamingTheAlternatives
and TestInsecureOptInDoesNotWarn), main_test.go
(TestBarePasswordIsRefusedOnEveryCommand) and
cmd/sweep/password_flag_test.go (TestSweepNeverPassesARefusedPasswordFlag).
Neither arrangement is automatically enforced. The extension's test is a Mocha
suite requiring a VS Code test host, so go test ./... never touches it, and this
project has no CI — the release gate runs the Go suite and the local gate scripts.
The four Go fixtures do run there. The TypeScript one is run by hand.
Workarounds#
There is a real one, it is complete for future invocations, and it covers nothing retroactively. On an affected extension with Mutant 2.5.0:
- Delete the
passwordproperty from every Mutant-type task in.vscode/tasks.json. With it unset, the generator emits no password flag at all and the CLI falls through to its prompt, which reads the password from the terminal with echo disabled. - If the task must be non-interactive, route the credential through the task's
argsarray instead:"args": ["--password-file", "<path>"]. The extension appendsargsverbatim at both affected versions, and 2.5.0 already declares--password-fileon the relevant commands. This needs no extension change and stays valid on 2.6.0, because it never uses the refused spelling.
What it does not cover:
- It is a replacement, not an addition. Leaving
passwordset and adding--password-fileinargsmakes the CLI refuse with "more than one password source given". The property must be removed. - It does not undo exposure. Any password that has already been on an argument
list, or written into a
tasks.json— above all a committed one — must be treated as disclosed and rotated. The workaround changes only what the next build does. --password-filemoves the secret from argv into a file; it does not protect it. Its protection is whatever the filesystem gives it, which on Windows means ACLs the operator has to set, and a file placed inside the workspace can be committed exactly astasks.jsoncould.--password-stdinis not reachable this way, because the task is a shell execution of a command and arguments with nothing to pipe into it.- For a run task there is nothing to work around at the affected versions: only the release arm ever emitted a password flag. Passing one for generate and run is something the fix added.
What changes for an operator#
On 2.6.0 the CLI refuses bare --password outright. So an operator running an
affected extension against 2.6.0 will find an unmodified release task failing
rather than leaking — the task emits a flag the CLI no longer accepts. That is the
practical consequence of there being no fixed extension release yet, and it is the
reason this page states both version numbers.
Credit#
Found by this project's own 2.6.0 security review, Stage B2, on 2026-09-25, and confirmed in Stage B4 the same day. No external reporter.
References#
- The entry in CHANGELOG.md under 2.6.0, Security, and the Changed entry for the CLI's refusal of the bare flag.
- SECURITY.md for the severity scale.
- The advisory register.