Skip to content

Commit 681b95d

Browse files
committed
feat(terminal): implement advanced shell command safety check #335
- Rewrite safety check using PSI (Program Structure Interface) to accurately parse and analyze shell commands - Add new dangerous patterns and improve existing ones- Introduce helper functions for better code organization and readability - Update TerminalSketchProvider to use the new safety check- Add unit tests for ShellSyntaxSafetyCheck to ensure correctness
1 parent 92e3b15 commit 681b95d

File tree

3 files changed

+127
-21
lines changed

3 files changed

+127
-21
lines changed
Lines changed: 82 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,96 @@
11
package cc.unitmesh.terminal.sketch
22

3-
object ShellSyntaxSafetyCheck {
3+
import com.intellij.openapi.project.Project
4+
import com.intellij.psi.PsiFileFactory
5+
import com.intellij.psi.util.PsiTreeUtil
6+
import com.intellij.sh.ShLanguage
7+
import com.intellij.sh.psi.ShCommand
8+
import com.intellij.sh.psi.ShFile
49

10+
object ShellSyntaxSafetyCheck {
511
/**
612
* Check if shell command contains dangerous operations
713
* @return Pair<Boolean, String> - first: is dangerous, second: reason message
814
*/
9-
fun checkDangerousCommand(command: String): Pair<Boolean, String> {
10-
val dangerousPatterns = mapOf(
11-
"\\brm\\s+(-[a-zA-Z]*f|-[a-zA-Z]*r|-[a-zA-Z]*(rf|fr))\\b.*".toRegex() to "Dangerous rm command with recursive or force flags",
12-
"\\brm\\s+-[a-zA-Z]*\\s+/\\b.*".toRegex() to "Removing files from root directory",
13-
"\\brmdir\\s+/\\b.*".toRegex() to "Removing directories from root",
14-
"\\bmkfs\\b.*".toRegex() to "Filesystem formatting command",
15-
"\\bdd\\b.*".toRegex() to "Low-level disk operation",
16-
"\\b:[(][)][{]\\s*:|:&\\s*[}];:.*".toRegex() to "Potential fork bomb",
17-
"\\bchmod\\s+-[a-zA-Z]*R\\b.*777\\b.*".toRegex() to "Recursive chmod with insecure permissions",
18-
"\\bsudo\\s+rm\\b.*".toRegex() to "Removing files with elevated privileges",
19-
)
20-
21-
// Also catch simpler rm commands (without flags but still potentially dangerous)
22-
if (command.trim().startsWith("rm ") && !command.contains("-i") && !command.contains("--interactive")) {
23-
return Pair(true, "Remove command detected, use with caution")
24-
}
15+
fun checkDangerousCommand(project: Project, command: String): Pair<Boolean, String> {
16+
val psiFile = PsiFileFactory.getInstance(project)
17+
.createFileFromText("temp.sh", ShLanguage.INSTANCE, command) as? ShFile
18+
?: return Pair(true, "Could not parse command")
2519

26-
for ((pattern, message) in dangerousPatterns) {
27-
if (pattern.containsMatchIn(command)) {
28-
return Pair(true, message)
20+
val commandElements = PsiTreeUtil.findChildrenOfType(psiFile, ShCommand::class.java)
21+
22+
for (cmd in commandElements) {
23+
if (isDangerousRmCommand(cmd)) {
24+
return Pair(true, "Dangerous rm command detected")
2925
}
26+
27+
if (isSudoCommand(cmd)) {
28+
val sudoArgs = getSudoArgs(cmd)
29+
if (sudoArgs.contains("rm")) {
30+
return Pair(true, "Removing files with elevated privileges")
31+
}
32+
}
33+
34+
if (isCommandWithName(cmd, "mkfs")) {
35+
return Pair(true, "Filesystem formatting command")
36+
}
37+
38+
if (isCommandWithName(cmd, "dd")) {
39+
return Pair(true, "Low-level disk operation")
40+
}
41+
42+
if (isCommandWithName(cmd, "chmod") && hasRecursiveFlag(cmd) && hasInsecurePermissions(cmd)) {
43+
return Pair(true, "Recursive chmod with insecure permissions")
44+
}
45+
46+
if (operatesOnRootDirectory(cmd)) {
47+
return Pair(true, "Operation targeting root directory")
48+
}
49+
}
50+
51+
if (command.contains(":(){ :|:& };:") || command.matches(".*:[(][)][{]\\s*:|:&\\s*[}];:.*".toRegex())) {
52+
return Pair(true, "Potential fork bomb")
3053
}
3154

3255
return Pair(false, "")
3356
}
57+
58+
private fun isDangerousRmCommand(command: ShCommand): Boolean {
59+
if (!isCommandWithName(command, "rm")) return false
60+
61+
val options = getCommandOptions(command)
62+
return options.contains("-rf") || options.contains("-fr") ||
63+
options.contains("-r") && options.contains("-f") ||
64+
options.contains("-f") && !options.contains("-i")
65+
}
66+
67+
private fun isSudoCommand(command: ShCommand): Boolean {
68+
return isCommandWithName(command, "sudo")
69+
}
70+
71+
private fun getSudoArgs(cmd: ShCommand): List<String> {
72+
return cmd.text.trim().split("\\s+".toRegex()).drop(1)
73+
}
74+
75+
private fun getCommandOptions(cmd: ShCommand): List<String> {
76+
return cmd.text.trim().split("\\s+".toRegex()).filter { it.startsWith("-") }
77+
}
78+
79+
private fun isCommandWithName(cmd: ShCommand, name: String): Boolean {
80+
val tokens = cmd.text.trim().split("\\s+".toRegex())
81+
return tokens.firstOrNull() == name
82+
}
83+
84+
private fun hasRecursiveFlag(cmd: ShCommand): Boolean {
85+
return getCommandOptions(cmd).any { it.matches("-[rR]+".toRegex()) }
86+
}
87+
88+
private fun hasInsecurePermissions(cmd: ShCommand): Boolean {
89+
return cmd.text.contains("777")
90+
}
91+
92+
private fun operatesOnRootDirectory(cmd: ShCommand): Boolean {
93+
val tokens = cmd.text.trim().split("\\s+".toRegex())
94+
return tokens.any { it == "/" }
95+
}
3496
}

exts/ext-terminal/src/main/kotlin/cc/unitmesh/terminal/sketch/TerminalSketchProvider.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ class TerminalLangSketch(val project: Project, var content: String) : ExtensionL
267267
codeSketch.updateViewText(code, true)
268268
titleLabel.text = "Terminal - ($content)"
269269

270-
val (isDangerous, reason) = ShellSyntaxSafetyCheck.checkDangerousCommand(content)
270+
val (isDangerous, reason) = ShellSyntaxSafetyCheck.checkDangerousCommand(project, content)
271271
if (isDangerous) {
272272
AutoDevNotifications.notify(project, "Auto-execution has been disabled for safety: $reason")
273273

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package cc.unitmesh.terminal.sketch
2+
3+
import com.intellij.testFramework.fixtures.BasePlatformTestCase
4+
5+
class ShellSyntaxSafetyCheckTest : BasePlatformTestCase() {
6+
fun testCheckDangerousCommandByPsi() {
7+
val project = project // Use the test fixture's project
8+
9+
// Test safe commands
10+
val safeCommands = listOf(
11+
"ls -la",
12+
"cd /home/user",
13+
"echo 'Hello World'"
14+
)
15+
16+
for (command in safeCommands) {
17+
val result = ShellSyntaxSafetyCheck.checkDangerousCommandByPsi(project, command)
18+
assertFalse("Should be safe: $command", result.first)
19+
assertEquals("", result.second)
20+
}
21+
22+
// Test dangerous commands
23+
val dangerousCommands = mapOf(
24+
"rm -rf /tmp" to "Dangerous rm command detected",
25+
"sudo rm file.txt" to "Removing files with elevated privileges",
26+
"mkfs /dev/sda1" to "Filesystem formatting command",
27+
"dd if=/dev/zero of=/dev/sda" to "Low-level disk operation",
28+
"chmod -R 777 /var" to "Recursive chmod with insecure permissions",
29+
"rm /" to "Operation targeting root directory"
30+
)
31+
32+
for ((command, expectedMessage) in dangerousCommands) {
33+
val result = ShellSyntaxSafetyCheck.checkDangerousCommandByPsi(project, command)
34+
assertTrue("Should be dangerous: $command", result.first)
35+
assertEquals(expectedMessage, result.second)
36+
}
37+
38+
// Test fork bomb
39+
val forkBomb = ":(){ :|:& };:"
40+
val result = ShellSyntaxSafetyCheck.checkDangerousCommandByPsi(project, forkBomb)
41+
assertTrue("Fork bomb should be detected", result.first)
42+
assertEquals("Potential fork bomb", result.second)
43+
}
44+
}

0 commit comments

Comments
 (0)