fix: path traversal in keystore v2 DirectoryBackend.osPath()#736
Open
zenmpi wants to merge 3 commits intocossacklabs:masterfrom
Open
fix: path traversal in keystore v2 DirectoryBackend.osPath()#736zenmpi wants to merge 3 commits intocossacklabs:masterfrom
zenmpi wants to merge 3 commits intocossacklabs:masterfrom
Conversation
## Vulnerability
The `osPath()` function in `DirectoryBackend` is responsible for
converting logical key paths into OS filesystem paths and ensuring
they remain within the keystore root directory.
The existing check compares `fullPath` with `filepath.Clean(fullPath)`:
if fullPath != filepath.Clean(fullPath) {
return "", api.ErrInvalidPath
}
However, `fullPath` is produced by `filepath.Join()`, which according
to Go documentation already returns a Clean-ed result. Therefore the
condition `fullPath != filepath.Clean(fullPath)` is always false,
and the check never rejects any input — including paths containing
"..".
This allows a crafted key path (e.g. via `clientID` containing
"../../") to resolve to a file outside the keystore root directory.
Note: the keystore v1 (`keystore/filesystem/`) is not affected because
it calls `ValidateID()` before constructing paths, which restricts
input to `[a-zA-Z0-9_\- ]`. The v2 keystore does not call `ValidateID`.
## Impact
An attacker who can supply an arbitrary `clientID` through the gRPC API
(when `acratranslator_client_id_from_connection_enable` is false, which
is the default) can read keyring files (*.keyring) from arbitrary
directories on the server. The file name suffix is fixed by the calling
code (e.g. "storage.keyring"), so arbitrary file read is not possible,
but keys belonging to other Acra instances on the same host can be
accessed.
When `acratranslator_client_id_from_connection_enable` is true, the
clientID is extracted from the TLS certificate and cannot be controlled
by the caller. The HTTP API is also not affected because it always
derives clientID from the TLS connection.
## Fix
Replace the ineffective `filepath.Clean` comparison with a
`strings.HasPrefix` check that verifies the resolved path starts with
the normalized root directory prefix. `filepath.Clean(b.root)` is used
to handle edge cases where the root path may contain a trailing slash.
Lagovas
reviewed
Feb 26, 2026
Move filepath.Clean(root) + PathSeparator computation from osPath() into CreateDirectoryBackend and OpenDirectoryBackend constructors. Store the result in the rootPrefix field to avoid redundant work on every osPath() invocation.
Lagovas
reviewed
Feb 27, 2026
Clean root with filepath.Clean() at construction time instead of storing raw root alongside a separate rootPrefix. The HasPrefix check in osPath() now uses b.root + separator directly. This also fixes a latent issue in ListAll() where TrimPrefix on line 352 needs root to match filepath.Walk output format.
Lagovas
approved these changes
Mar 2, 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.
Summary
The path traversal check in
DirectoryBackend.osPath()(keystore/v2/keystore/filesystem/backend/filesystem.go) is ineffective and does not prevent key path resolution outside the keystore root directory.Vulnerability Details
The function constructs an OS path from a logical key path and validates it:
According to the Go documentation,
filepath.Joinalready returns aClean-ed result:Therefore
fullPath != filepath.Clean(fullPath)is alwaysfalse, and the check never fires — even when the resolved path escapes the root directory.Example
Affected Path
The keystore v1 (
keystore/filesystem/) callsValidateID()which restrictsclientIDto[a-zA-Z0-9_\- ], effectively blocking..in paths. The v2 keystore does not callValidateID(), relying solely on theosPath()check which is ineffective.Impact
When
clientIDis taken directly from the gRPC request (acratranslator_client_id_from_connection_enable=false, the default), a craftedclientIDcontaining..sequences can cause the keystore to read keyring files from outside the root directory. The file name suffix (e.g.storage.keyring) is appended by the calling code and cannot be controlled, so this is limited to reading*.keyringfiles from arbitrary directories — not arbitrary file read.When
acratranslator_client_id_from_connection_enable=true, theclientIDis extracted from the TLS peer certificate and is not attacker-controlled. The HTTP API always extractsclientIDfrom the TLS connection and is not affected.Fix
Two changes:
1. Sanitize
rootin both constructors (CreateDirectoryBackend,OpenDirectoryBackend):This ensures
rootis always in canonical form. This also fixes a latent issue inListAll()wherestrings.TrimPrefix(path, b.root+separator)needsrootto matchfilepath.Walkoutput format —Walkreturns cleaned paths, so ifrootwas not cleaned,TrimPrefixcould silently fail to strip the prefix.2. Replace the tautological
filepath.Cleancomparison inosPath()withstrings.HasPrefix:The
+ string(os.PathSeparator)suffix prevents partial prefix matches — e.g. a root of/keysmust not match a path under/keys-backup.Testing
Verified with path traversal inputs (
../,../../,client/../../) that:/keys-evil) are handled correctly