GHSA-5WP8-Q9MX-8JX8

Vulnerability from github – Published: 2026-03-05 00:38 – Updated: 2026-03-05 00:38
VLAI
Summary
zeptoclaw has Shell allowlist-blocklist bypass via command/argument injection and file name wildcards
Details

Summary

zeptoclaw implements a allowlist combined with a blocklist to prevent malicious shell commands in src/security/shell.rs. However, even in the Strict mode, attackers can completely bypass all the guards from allowlist and blocklist:

  • to bypass the allowlist, command injection is enough, such as ;, $() etc.
  • to bypass the REGEX_BLOCKED_PATTERNS, argument injection is enough, such as the python3 -P -c "..."
  • to bypass the LITERAL_BLOCKED_PATTERNS, file name wildcards can do the work, such as cat /etc/pass[w]d

Details

In code src/security/shell.rs#L218-L243, one can see the allowlist only checks the first token and thus makes command injection possible.

        // Allowlist check (runs after blocklist)
        if self.allowlist_mode != ShellAllowlistMode::Off && !self.allowlist.is_empty() {
            let first_token = command
                .split_whitespace()
                .next()
                .unwrap_or("")
                .to_lowercase();
            // Strip path prefix (e.g. /usr/bin/git -> git)
            let executable = first_token.rsplit('/').next().unwrap_or(&first_token);
            if !self.allowlist.iter().any(|a| a == executable) {
                match self.allowlist_mode {
                    ShellAllowlistMode::Strict => {
                        return Err(ZeptoError::SecurityViolation(format!(
                            "Command '{}' not in allowlist",
                            executable
                        )));
                    }
                    ShellAllowlistMode::Warn => {
                        tracing::warn!(
                            command = %command,
                            executable = %executable,
                            "Command not in allowlist"
                        );
                    }
                    ShellAllowlistMode::Off => {} // unreachable
                }

!self.allowlist.is_empty() makes the empty allowlist overlook the allowlist check, if it is in ShellAllowlistMode::Strict mode, empty allowlist should direct reject all the commands.

As the code in src/security/shell.rs#L18-L70, we can find the REGEX_BLOCKED_PATTERNS only apply \s+ in between the command and arguments, making argument injection possible, and the LITERAL_BLOCKED_PATTERNS just uses specific file name, totally overlooking the file name wildcards:

const REGEX_BLOCKED_PATTERNS: &[&str] = &[
    // Piped shell execution (curl/wget to sh/bash)
    r"curl\s+.*\|\s*(sh|bash|zsh)",
    r"wget\s+.*\|\s*(sh|bash|zsh)",
    r"\|\s*(sh|bash|zsh)\s*$",
    // Reverse shells
    r"bash\s+-i\s+>&\s*/dev/tcp",
    r"nc\s+.*-e\s+(sh|bash|/bin)",
    r"/dev/tcp/",
    r"/dev/udp/",
    // Destructive root operations (various flag orderings)
    r"rm\s+(-[rf]{1,2}\s+)*(-[rf]{1,2}\s+)*/\s*($|;|\||&)",
    r"rm\s+(-[rf]{1,2}\s+)*(-[rf]{1,2}\s+)*/\*\s*($|;|\||&)",
    // Format/overwrite disk
    r"mkfs(\.[a-z0-9]+)?\s",
    r"dd\s+.*if=/dev/(zero|random|urandom).*of=/dev/[sh]d",
    r">\s*/dev/[sh]d[a-z]",
    // System-wide permission changes
    r"chmod\s+(-R\s+)?777\s+/\s*$",
    r"chmod\s+(-R\s+)?777\s+/[a-z]",
    // Fork bombs
    r":\(\)\s*\{\s*:\|:&\s*\}\s*;:",
    r"fork\s*\(\s*\)",
    // Encoded/indirect execution (common blocklist bypasses)
    r"base64\s+(-d|--decode)",
    r"python[23]?\s+-c\s+",
    r"perl\s+-e\s+",
    r"ruby\s+-e\s+",
    r"node\s+-e\s+",
    r"\beval\s+",
    r"xargs\s+.*sh\b",
    r"xargs\s+.*bash\b",
    // Environment variable exfiltration
    r"\benv\b.*>\s*/",
    r"\bprintenv\b.*>\s*/",
];

/// Literal substring patterns (credentials, sensitive paths)
const LITERAL_BLOCKED_PATTERNS: &[&str] = &[
    "/etc/shadow",
    "/etc/passwd",
    "~/.ssh/",
    ".ssh/id_rsa",
    ".ssh/id_ed25519",
    ".ssh/id_ecdsa",
    ".ssh/id_dsa",
    ".ssh/authorized_keys",
    ".aws/credentials",
    ".kube/config",
    // ZeptoClaw's own config (contains API keys and channel tokens)
    ".zeptoclaw/config.json",
    ".zeptoclaw/config.yaml",
];

PoC

    #[test]
    fn test_allowlist_bypass() {
        let config =
            ShellSecurityConfig::new().with_allowlist(vec!["git"], ShellAllowlistMode::Strict);
        assert!(config.validate_command("/usr/bin/git status; python -P -c 'import os; os.system(\"rm -rf /\")'; cat /etc/pass[w]d").is_ok());
    }

Impact

Unauthorized command execution.

Credit

@zpbrent

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 0.6.1"
      },
      "package": {
        "ecosystem": "crates.io",
        "name": "zeptoclaw"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.6.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-77"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-03-05T00:38:14Z",
    "nvd_published_at": null,
    "severity": "CRITICAL"
  },
  "details": "### Summary\n[zeptoclaw](https://github.com/qhkm/zeptoclaw) implements a allowlist combined with a blocklist to prevent malicious shell commands in [src/security/shell.rs](https://github.com/qhkm/zeptoclaw/blob/v0.5.8/src/security/shell.rs). However, even in the `Strict` mode, attackers can completely bypass all the guards from allowlist and blocklist:\n\n- to bypass the `allowlist`, command injection is enough, such as `;`, `$()` etc.\n- to bypass the `REGEX_BLOCKED_PATTERNS`, argument injection is enough, such as the `python3 -P -c \"...\"`\n- to bypass the `LITERAL_BLOCKED_PATTERNS`, file name wildcards can do the work, such as `cat /etc/pass[w]d`\n\n### Details\nIn code [src/security/shell.rs#L218-L243](https://github.com/qhkm/zeptoclaw/blob/fe2ef07cfec5bb46b42cdd65f52b9230c03e9270/src/security/shell.rs#L218-L243), one can see the allowlist only checks the first token and thus makes command injection possible. \n```rust\n        // Allowlist check (runs after blocklist)\n        if self.allowlist_mode != ShellAllowlistMode::Off \u0026\u0026 !self.allowlist.is_empty() {\n            let first_token = command\n                .split_whitespace()\n                .next()\n                .unwrap_or(\"\")\n                .to_lowercase();\n            // Strip path prefix (e.g. /usr/bin/git -\u003e git)\n            let executable = first_token.rsplit(\u0027/\u0027).next().unwrap_or(\u0026first_token);\n            if !self.allowlist.iter().any(|a| a == executable) {\n                match self.allowlist_mode {\n                    ShellAllowlistMode::Strict =\u003e {\n                        return Err(ZeptoError::SecurityViolation(format!(\n                            \"Command \u0027{}\u0027 not in allowlist\",\n                            executable\n                        )));\n                    }\n                    ShellAllowlistMode::Warn =\u003e {\n                        tracing::warn!(\n                            command = %command,\n                            executable = %executable,\n                            \"Command not in allowlist\"\n                        );\n                    }\n                    ShellAllowlistMode::Off =\u003e {} // unreachable\n                }\n```\n `!self.allowlist.is_empty()` makes the empty allowlist overlook the allowlist check, if it is in `ShellAllowlistMode::Strict` mode, empty allowlist should direct reject all the commands.\n\nAs the code in [src/security/shell.rs#L18-L70](https://github.com/qhkm/zeptoclaw/blob/fe2ef07cfec5bb46b42cdd65f52b9230c03e9270/src/security/shell.rs#L18-L70), we can find the `REGEX_BLOCKED_PATTERNS` only apply `\\s+` in between the command and arguments, making argument injection possible, and the `LITERAL_BLOCKED_PATTERNS` just uses specific file name, totally overlooking the file name wildcards:\n```rust\nconst REGEX_BLOCKED_PATTERNS: \u0026[\u0026str] = \u0026[\n    // Piped shell execution (curl/wget to sh/bash)\n    r\"curl\\s+.*\\|\\s*(sh|bash|zsh)\",\n    r\"wget\\s+.*\\|\\s*(sh|bash|zsh)\",\n    r\"\\|\\s*(sh|bash|zsh)\\s*$\",\n    // Reverse shells\n    r\"bash\\s+-i\\s+\u003e\u0026\\s*/dev/tcp\",\n    r\"nc\\s+.*-e\\s+(sh|bash|/bin)\",\n    r\"/dev/tcp/\",\n    r\"/dev/udp/\",\n    // Destructive root operations (various flag orderings)\n    r\"rm\\s+(-[rf]{1,2}\\s+)*(-[rf]{1,2}\\s+)*/\\s*($|;|\\||\u0026)\",\n    r\"rm\\s+(-[rf]{1,2}\\s+)*(-[rf]{1,2}\\s+)*/\\*\\s*($|;|\\||\u0026)\",\n    // Format/overwrite disk\n    r\"mkfs(\\.[a-z0-9]+)?\\s\",\n    r\"dd\\s+.*if=/dev/(zero|random|urandom).*of=/dev/[sh]d\",\n    r\"\u003e\\s*/dev/[sh]d[a-z]\",\n    // System-wide permission changes\n    r\"chmod\\s+(-R\\s+)?777\\s+/\\s*$\",\n    r\"chmod\\s+(-R\\s+)?777\\s+/[a-z]\",\n    // Fork bombs\n    r\":\\(\\)\\s*\\{\\s*:\\|:\u0026\\s*\\}\\s*;:\",\n    r\"fork\\s*\\(\\s*\\)\",\n    // Encoded/indirect execution (common blocklist bypasses)\n    r\"base64\\s+(-d|--decode)\",\n    r\"python[23]?\\s+-c\\s+\",\n    r\"perl\\s+-e\\s+\",\n    r\"ruby\\s+-e\\s+\",\n    r\"node\\s+-e\\s+\",\n    r\"\\beval\\s+\",\n    r\"xargs\\s+.*sh\\b\",\n    r\"xargs\\s+.*bash\\b\",\n    // Environment variable exfiltration\n    r\"\\benv\\b.*\u003e\\s*/\",\n    r\"\\bprintenv\\b.*\u003e\\s*/\",\n];\n\n/// Literal substring patterns (credentials, sensitive paths)\nconst LITERAL_BLOCKED_PATTERNS: \u0026[\u0026str] = \u0026[\n    \"/etc/shadow\",\n    \"/etc/passwd\",\n    \"~/.ssh/\",\n    \".ssh/id_rsa\",\n    \".ssh/id_ed25519\",\n    \".ssh/id_ecdsa\",\n    \".ssh/id_dsa\",\n    \".ssh/authorized_keys\",\n    \".aws/credentials\",\n    \".kube/config\",\n    // ZeptoClaw\u0027s own config (contains API keys and channel tokens)\n    \".zeptoclaw/config.json\",\n    \".zeptoclaw/config.yaml\",\n];\n```\n\n### PoC\n```rust\n    #[test]\n    fn test_allowlist_bypass() {\n        let config =\n            ShellSecurityConfig::new().with_allowlist(vec![\"git\"], ShellAllowlistMode::Strict);\n        assert!(config.validate_command(\"/usr/bin/git status; python -P -c \u0027import os; os.system(\\\"rm -rf /\\\")\u0027; cat /etc/pass[w]d\").is_ok());\n    }\n```\n\n### Impact\nUnauthorized command execution.\n\n### Credit\n[@zpbrent](https://github.com/zpbrent)",
  "id": "GHSA-5wp8-q9mx-8jx8",
  "modified": "2026-03-05T00:38:14Z",
  "published": "2026-03-05T00:38:14Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/qhkm/zeptoclaw/security/advisories/GHSA-5wp8-q9mx-8jx8"
    },
    {
      "type": "WEB",
      "url": "https://github.com/qhkm/zeptoclaw/commit/68916c3e4f3af107f11940b27854fc7ef517058b"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/qhkm/zeptoclaw"
    },
    {
      "type": "WEB",
      "url": "https://github.com/qhkm/zeptoclaw/blob/fe2ef07cfec5bb46b42cdd65f52b9230c03e9270/src/security/shell.rs#L218-L243"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "zeptoclaw has Shell allowlist-blocklist bypass via command/argument injection and file name wildcards"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…