Skip to content

Security: unvalidated plugin name allows arbitrary executable path in Process.Start #11

Description

@pscheid92

Summary

A plugin recipient (age1<name>1…) or identity (AGE-PLUGIN-<NAME>-1…) string carries the plugin name in the bech32 HRP. PluginRecipient.ExtractPluginName / PluginIdentity.ExtractPluginName return that name without validating its character set, and PluginConnection interpolates it into age-plugin-<name> which is handed to Process.Start (Age/Plugin/PluginConnection.cs).

Bech32.Decode validates the checksum, mixed-case rule, and the data character set — but not the HRP character set. So a name containing a path separator (/, or \ on Windows) passes through intact. Because .NET's Process.Start (with UseShellExecute = false) treats a FileName containing a directory separator as a path relative to the working directory rather than doing a PATH lookup, a crafted recipient/identity can make AgeSharp execute an arbitrary binary.

Impact

Arbitrary code execution. An attacker who gets a victim to (a) use an attacker-supplied recipient (encryption) or identity (decryption) — identity files are routinely shared for decryption — and (b) place a file at a predictable relative path (e.g. age-plugin-<x>/<y> under the victim's working directory) achieves code execution when the victim runs an age operation. The age-plugin- prefix prevents absolute-path injection, but CWD-relative execution via / is sufficient.

Proof of concept

Both strings below are valid bech32 with valid checksums (an attacker computes the checksum trivially):

  • Recipient age1pwn/pwn1qypqx5g6mypExtractPluginName returns pwn/pwn → binary path age-plugin-pwn/pwn.
  • Identity AGE-PLUGIN-PWN/PWN-1QY06FDQA → same pwn/pwn.

Confirmed with a local test: the separator survives decoding into the name used for Process.Start.

Root cause

No allowlist validation on the plugin name; Bech32.Decode does not restrict HRP characters.

Reference implementation

Upstream age validates this. plugin/encode.go validPluginName restricts names to abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-._ (no path separators), enforced in both ParseRecipient and ParseIdentity. There is an explicit CLI test for it (cmd/age/testdata/plugin.txt: "check that path separators are rejected", with an age-plugin-pwn/pwn payload).

Suggested fix

  • Enforce a validPluginName-style allowlist ([A-Za-z0-9+\-._], i.e. no /, \, or other separators/specials) in ExtractPluginName for both PluginRecipient and PluginIdentity, throwing on violation so the malicious string is rejected at parse time, before any Process.Start.
  • Add regression tests using the PoC strings above (recipient and identity), asserting rejection.

Context

Found while working on the plugin confirm fixes (#6). Not introduced by that work — this is a pre-existing gap.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions