Skip to content

Validate plugin names to prevent arbitrary executable paths (#11)#13

Merged
pscheid92 merged 3 commits into
mainfrom
fix/plugin-name-validation
Jul 23, 2026
Merged

Validate plugin names to prevent arbitrary executable paths (#11)#13
pscheid92 merged 3 commits into
mainfrom
fix/plugin-name-validation

Conversation

@pscheid92

@pscheid92 pscheid92 commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Problem

A plugin recipient (age1<name>1…) / identity (AGE-PLUGIN-<NAME>-1…) carries the plugin name in its bech32 HRP. ExtractPluginName returned it unvalidated, and PluginConnection interpolates it into age-plugin-<name> for Process.Start. Bech32.Decode validates the checksum and data charset but not the HRP character set, so a name containing a path separator survives.

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 a PATH lookup, a crafted string could make AgeSharp execute an arbitrary binary. PoC (valid bech32 + checksum): age1pwn/pwn1qypqx5g6myp → name pwn/pwnage-plugin-pwn/pwn executed from CWD. See #11.

Fix

  • New PluginNameValidator with an allowlist matching age's validPluginName: [A-Za-z0-9+-._], which excludes /, \, and other separators.
  • Enforced in ExtractPluginName for both PluginRecipient and PluginIdentity, so a malicious string is rejected at construction — before any process launch (the CLI builds these objects, so it's covered too).
  • Defense in depth: PluginConnection also refuses an invalid name right at the Process.Start sink.
  • Validation lives at the plugin layer, not in Bech32.Decode — bech32 is a generic codec, and per BIP-173 the HRP legally allows / anyway, so a codec-level fix wouldn't address this.

Tests

New regression tests: recipient and identity names containing /, \, or a space are rejected; the constructor throws before launch; PluginConnection refuses an invalid name; valid names (yubikey, fido2hmac, se-cure_plugin.v2) still parse. Full suite green locally (Age.Tests 335/335, Age.TestKit 143/143, 0 skipped); runs on the 3-OS matrix.

Fixes #11

A plugin recipient/identity carries the plugin name in its bech32 HRP.
Bech32.Decode validates the checksum and data charset but not the HRP,
so a name could contain a path separator, which then flowed into
'age-plugin-<name>' and Process.Start. Because .NET treats a FileName
containing a separator as a path relative to the working directory
(rather than a PATH lookup), a crafted recipient/identity could execute
an arbitrary binary (e.g. 'age1pwn/pwn1...' -> 'age-plugin-pwn/pwn').

- Add PluginNameValidator with an allowlist matching age's
  validPluginName ([A-Za-z0-9+-._]; no path separators).
- Enforce it in ExtractPluginName for both PluginRecipient and
  PluginIdentity, so a bad name is rejected at construction, before
  any process launch.
- Defense in depth: PluginConnection also refuses an invalid name at
  the Process.Start sink.
- Add regression tests (path separators, backslash, space rejected;
  valid names still accepted).

Fixes #11
@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 91.57%. Comparing base (6825724) to head (ee7da42).

Files with missing lines Patch % Lines
Age/Plugin/PluginConnection.cs 50.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #13      +/-   ##
==========================================
+ Coverage   91.46%   91.57%   +0.10%     
==========================================
  Files          37       38       +1     
  Lines        2320     2338      +18     
  Branches      304      310       +6     
==========================================
+ Hits         2122     2141      +19     
+ Misses        141      139       -2     
- Partials       57       58       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

ExtractPluginName sliced the decoded HRP (hrp[4..] / hrp[11..^1]) after
only a loose StartsWith check, so a degenerate HRP threw
ArgumentOutOfRangeException instead of a clean FormatException:
  - recipient: an HRP of just "age" -> hrp[4..]
  - identity: an HRP of exactly "age-plugin-" -> hrp[11..^1]

Require the full expected shape ('age1<name>' / 'age-plugin-<name>-')
before slicing, so malformed input yields FormatException. Add
regression tests for both.
@pscheid92
pscheid92 merged commit 61a9546 into main Jul 23, 2026
5 checks passed
@pscheid92
pscheid92 deleted the fix/plugin-name-validation branch July 23, 2026 01:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant