-
Notifications
You must be signed in to change notification settings - Fork 28
fix: block critical recursive delete paths #524
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -631,13 +631,26 @@ function rmTargetIsAllowed(cmd: string, allowPaths: string[]): boolean { | |||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| function isCriticalRecursiveDeletePath(token: string): boolean { | ||||||||||||||||||||||||||||||||||
| const normalized = token.replace(/\/\*$/, "").replace(/\/+$/, "") || (token.startsWith("/") ? "/" : ""); | ||||||||||||||||||||||||||||||||||
| return normalized === "/" | ||||||||||||||||||||||||||||||||||
| || normalized === "~" | ||||||||||||||||||||||||||||||||||
| || normalized === "$HOME" | ||||||||||||||||||||||||||||||||||
| || normalized === "${HOME}" | ||||||||||||||||||||||||||||||||||
| || normalized.startsWith("~/") | ||||||||||||||||||||||||||||||||||
| || /^\/[A-Za-z_][\w.-]*$/.test(normalized) | ||||||||||||||||||||||||||||||||||
| || /^\/(?:home|Users)\/[^/]+$/.test(normalized) | ||||||||||||||||||||||||||||||||||
| || /^\/(?:var\/(?:lib|log)|usr\/(?:lib|local))$/.test(normalized); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| function blockRmRf(ctx: PolicyContext): PolicyResult { | ||||||||||||||||||||||||||||||||||
| if (ctx.toolName !== "Bash") return allow(); | ||||||||||||||||||||||||||||||||||
| const cmd = getCommand(ctx); | ||||||||||||||||||||||||||||||||||
| const hasDestructivePath = parseArgvTokens(cmd).some((token) => { | ||||||||||||||||||||||||||||||||||
| const normalized = token.replace(/\/\*$/, "").replace(/\/+$/, "") || (token.startsWith("/") ? "/" : ""); | ||||||||||||||||||||||||||||||||||
| return normalized === "/" || normalized === "~" || /^\/[A-Za-z_][\w.-]*$/.test(normalized); | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
| const hasDestructivePath = parseArgvTokens(cmd).some(isCriticalRecursiveDeletePath); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (/\bfind\s+(?:\/|~|\$HOME|\$\{HOME\})(?=\s|$)[\s\S]*?\s-delete\b/.test(cmd)) { | ||||||||||||||||||||||||||||||||||
| return deny("Catastrophic deletion blocked"); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+649
to
+653
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Refine The current regex for
You can fix all of these by leveraging the parsed 🛡️ Proposed fix- const hasDestructivePath = parseArgvTokens(cmd).some(isCriticalRecursiveDeletePath);
-
- if (/\bfind\s+(?:\/|~|\$HOME|\$\{HOME\})(?=\s|$)[\s\S]*?\s-delete\b/.test(cmd)) {
- return deny("Catastrophic deletion blocked");
- }
+ const tokens = parseArgvTokens(cmd);
+ const hasDestructivePath = tokens.some(isCriticalRecursiveDeletePath);
+
+ const findIdx = tokens.indexOf("find");
+ const deleteIdx = tokens.indexOf("-delete");
+ if (findIdx !== -1 && deleteIdx > findIdx) {
+ const findArgs = tokens.slice(findIdx + 1, deleteIdx);
+ if (findArgs.some(isCriticalRecursiveDeletePath)) {
+ return deny("Catastrophic deletion blocked");
+ }
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Combined flags in one token: rm -rf /, rm -fr / | ||||||||||||||||||||||||||||||||||
| if (hasDestructivePath && ( | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Fix path normalization to prevent bypasses and include
$HOMErelative paths.//usr,//var/lib, or/var//libwill bypass the exact regex matches because multiple consecutive slashes are not collapsed (e.g.,//usris perfectly valid in Bash but fails/^\/[A-Za-z_]/).$HOME/coverage: The logic correctly covers~/withnormalized.startsWith("~/"), but misses$HOME/and${HOME}/. This leavesrm -rf $HOME/Documentsexposed, contradicting the PR objective to protect home-relative paths.Normalize paths by collapsing consecutive slashes, and add checks for
$HOME/and${HOME}/.🛡️ Proposed fix
function isCriticalRecursiveDeletePath(token: string): boolean { - const normalized = token.replace(/\/\*$/, "").replace(/\/+$/, "") || (token.startsWith("/") ? "/" : ""); + const normalized = token.replace(/\/{2,}/g, "/").replace(/\/\*$/, "").replace(/\/+$/, "") || (token.startsWith("/") ? "/" : ""); return normalized === "/" || normalized === "~" || normalized === "$HOME" || normalized === "${HOME}" || normalized.startsWith("~/") + || normalized.startsWith("$HOME/") + || normalized.startsWith("${HOME}/") || /^\/[A-Za-z_][\w.-]*$/.test(normalized) || /^\/(?:home|Users)\/[^/]+$/.test(normalized) || /^\/(?:var\/(?:lib|log)|usr\/(?:lib|local))$/.test(normalized); }📝 Committable suggestion
🤖 Prompt for AI Agents