Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
### Fixed
- **Commander compatibility** for current folder and record discriminators (`classic_folder` / `nested_share_folder`, `Classic` / `Nested`), with support for older wire values.

## [2.0.3] - 2026-07-23

### Fixed
- **Security (VM-1450 / CWE-78):** Reject `\r`, `\n`, and NUL in editor selection and related CLI inputs before piping to the persistent Keeper shell; escape quotes when embedding values in Commander commands; refuse multi-line writes at the shell boundary. Closes command injection via crafted Add Keeper Record selection (and the same pattern on Update / Generate). Update Record now also requires a valid 22-character record UID before lookup.

## [2.0.2] - 2026-07-07

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ pluginGroup = com.keepersecurity.jetbrains
pluginName = Keeper Security
pluginRepositoryUrl = https://github.com/Keeper-Security/keeper-jetbrains-plugin
# SemVer format -> https://semver.org
pluginVersion = 2.0.2
pluginVersion = 2.0.3

# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild = 243
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,12 @@ class KeeperGenerateSecretsAction : AnAction("Keeper Generate Secrets") {
project: Project
): String? {
return try {
// Reject control characters before embedding so a malformed CLI
// password cannot split the persistent-shell write into extra commands.
val safePassword = KeeperCliSafety.requireSafe(password, "generated password")
// The password literal is the same for both branches; it must be
// single-quoted so the CLI doesn't try to expand $-prefixed chars.
val passwordField = "password='${KeeperCliSafety.escapeSingleQuoted(password)}'"
val passwordField = "password='${KeeperCliSafety.escapeSingleQuoted(safePassword)}'"
val command = when (target) {
is GenerateTarget.Classic -> buildClassicAddCommand(title, passwordField, target.folderUuid)
is GenerateTarget.Drive -> buildDriveAddCommand(title, passwordField, target.folderUuid)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ class KeeperRecordUpdateAction : AnAction("Update Keeper Record") {
return
}

if (!KeeperCliSafety.isValidRecordUid(recordUid)) {
showError(
"Record uid must be a 22-character Keeper UID (letters, digits, '_' or '-').",
project
)
return
}

// Look up the UID in the vault to decide which CLI command to use.
// `list --format json` includes a `record_category` discriminator
// (`Classic` / `Nested`, plus older legacy wire values) that maps 1:1 onto
Expand Down
42 changes: 42 additions & 0 deletions src/test/kotlin/keepersecurity/util/KeeperCliSafetyTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,46 @@ class KeeperCliSafetyTest {
@Test fun `escapeSingleQuoted escapes embedded single quotes`() {
assertEquals("it'\\''s", KeeperCliSafety.escapeSingleQuoted("it's"))
}

// --- VM-1450: crafted editor selection / CLI injection regression shapes ---

@Test fun `requireSafe rejects Step-A shaped multi-line selection with quote break`() {
// Report Step A: close the field quote, inject a second Commander command, reopen.
val payload = "benign-value\"\nthis-device rename INJECTED\n\"end"
assertThrows(KeeperCliSafety.UnsafeCliInputException::class.java) {
KeeperCliSafety.requireSafe(payload, "selected text")
}
}

@Test fun `requireSafe rejects Step-B shaped multi-line selection with pam tunnel run`() {
val payload =
"benign-value\"\npam tunnel start abc123def456GHI789jkLM --run \"echo pwned\"\n\"end"
assertThrows(KeeperCliSafety.UnsafeCliInputException::class.java) {
KeeperCliSafety.requireSafe(payload, "selected text")
}
}

@Test fun `assertSingleLine rejects assembled record-add with injected second command`() {
val command =
"record-add --title=\"t\" --record-type=login password=\"benign-value\"\nthis-device rename INJECTED"
assertThrows(KeeperCliSafety.UnsafeCliInputException::class.java) {
KeeperCliSafety.assertSingleLine(command)
}
}

@Test fun `escapeDoubleQuoted prevents quote-break of field value`() {
val selection = "benign-value\""
val esc = KeeperCliSafety.escapeDoubleQuoted(selection)
assertEquals("benign-value\\\"", esc)
// After escaping, embedding inside double quotes must not prematurely close.
val field = "password=\"$esc\""
assertEquals("password=\"benign-value\\\"\"", field)
assertFalse(field.contains("password=\"benign-value\"\""))
}

@Test fun `isValidRecordUid rejects quote and metachar injection shapes`() {
assertFalse(KeeperCliSafety.isValidRecordUid("abc123def456GHI789jk\"M"))
assertFalse(KeeperCliSafety.isValidRecordUid("uid\" --folder=evil"))
assertFalse(KeeperCliSafety.isValidRecordUid("aaaaaaaaaaaaaaaaaaaaa\n"))
}
}
Loading