Validate plugin names to prevent arbitrary executable paths (#11)#13
Merged
Conversation
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 Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
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.
This was referenced Jul 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A plugin recipient (
age1<name>1…) / identity (AGE-PLUGIN-<NAME>-1…) carries the plugin name in its bech32 HRP.ExtractPluginNamereturned it unvalidated, andPluginConnectioninterpolates it intoage-plugin-<name>forProcess.Start.Bech32.Decodevalidates the checksum and data charset but not the HRP character set, so a name containing a path separator survives.Because .NET's
Process.Start(withUseShellExecute = false) treats aFileNamecontaining a directory separator as a path relative to the working directory rather than aPATHlookup, a crafted string could make AgeSharp execute an arbitrary binary. PoC (valid bech32 + checksum):age1pwn/pwn1qypqx5g6myp→ namepwn/pwn→age-plugin-pwn/pwnexecuted from CWD. See #11.Fix
PluginNameValidatorwith an allowlist matching age'svalidPluginName:[A-Za-z0-9+-._], which excludes/,\, and other separators.ExtractPluginNamefor bothPluginRecipientandPluginIdentity, so a malicious string is rejected at construction — before any process launch (the CLI builds these objects, so it's covered too).PluginConnectionalso refuses an invalid name right at theProcess.Startsink.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;PluginConnectionrefuses 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