Shell Script Scanner
Deploy, CI and cron scripts run with more privilege than application code and get less review. This scanner reads every .sh and .bash file and applies eight regular expressions to the raw text: hardcoded secrets, curl piped to a shell, eval on a variable, permissive chmod, missing error handling, printed secrets, disabled TLS and hardcoded AWS account IDs.
Which files it reads
By extension only. The scanner never opens a file to look for a shebang.
1The scanner starts from the repository file index. It does not read the first line of a file, so a script is a script only by its extension.
2Paths ending in .sh or .bash are scanned, and each one gets all eight checks.
3Shell code anywhere else is not seen: .zsh files, extensionless scripts under bin/, Makefile recipes and the run: blocks in CI workflows. Those are common places for the same mistakes.
Walking a script
Eight checks over one string, in a fixed order. A bar marks the text each one matched.
1One short deploy script, nine lines. Eight checks run in a fixed order over the raw text. There is no shell parsing: no quoting rules, no variable tracking, no control flow.
2Hardcoded secret. Eight patterns for named variables: AWS secret key, Stripe live key, database URL, Supabase service key, anything ending in PASSWORD (8+ characters), a GitHub token, API_KEY (20+), SECRET (16+). The value is then run through the placeholder list.
3AWS account ID. Twelve digits followed by .dkr.ecr.. An account id is not a secret, but it ties the script to one account and tells a reader which one.
4curl to a shell. curl, then anything but a pipe, then | bash, | sh or | zsh. The script runs whatever the server sends today.
5Permissive chmod. chmod 777, 666 or a+rwx. Any user on the machine can change a file that may later run as root.
6eval with a variable. eval followed by anything on the same line that contains a dollar sign. If the variable is attacker-influenced, this is command injection.
7Error handling. The check searches the file for set -e, set -u or set -o (any of the three letters after set -). With none present, a failed command is ignored and the script carries on.
8Secret to stdout. echo, then password, secret, token or key (any case), then : or =. CI logs keep what a script prints.
9TLS off. curl with -k or --insecure on the same line. The connection can be read and rewritten by anyone on the path.
10Eight findings. Line 9, rm -rf $DEPLOY_DIR, is not reported. The markdown lists unquoted variables, but no code checks for them.
What it deliberately skips
Each row is a line the scanner meets. Step through for the verdict and the reason. Several rows are blind spots rather than safeguards, and the page says so.
1References are not secrets. The password pattern's value class excludes $, so "$VAULT_PASSWORD" never matches.
2Placeholders. The skip list is applied to every matched value. It also drops values that start with http:// or https://.
3The slash rule. Any value with a / is treated as a path and skipped. That keeps file paths out of the findings and also hides real AWS secret keys, which are base64 and often contain a slash.
4Length floors. 8 for passwords, 20 for API keys and 16 for generic secrets. They keep short names and flags from matching.
5Comments count. The check for error handling looks for the text, so set -e inside a comment satisfies it. Treat a missing-line rule as a first filter.
6Exact values. The chmod check names the three dangerous modes. 755 and 700 pass.
7eval needs a dollar sign. The pattern looks for eval followed later on the line by $, so a constant string is quiet.
8Unquoted variables are not checked. rm -rf $DIR is a classic shell hazard, and it does not fire today.
The fix, line by line
The fixed form is the one the regular expressions do not match.
1Before: the eight findings from the walk. Each step fixes one line and the matching check goes quiet.
2Fetch it at run time. The value is a command substitution, not a literal, and the password pattern excludes $.
3Take the id from configuration. One script then works in every account.
4Download, inspect, then run. The pipe is gone, so the pattern cannot match.
5Use the least permission that works. 750 or 755 for directories, 700 for private files.
6Allow a fixed list of commands. No eval, no injection point.
7Fail on errors, unset variables and broken pipes. Any of the three flags clears the finding.
8Say that it worked, not what it is. The pattern needs a colon or equals sign after the word.
9Trust the right CA instead of nobody. Eight fixes, zero findings.
Why it matters
Shell scripts often run with elevated privileges in CI/CD, deployment and cron jobs. eval with user input is remote code execution on your server. A curl pipe to a shell runs whatever a compromised URL serves. Credentials in a script sit in version control history for anyone with read access.
SolarWinds (2020). The attack vector was a supply chain compromise, but the principle is the same: build and deploy scripts that execute remote or dynamic content without verification are high-risk targets.
Vulnerable vs fixed code
# BAD: eval, curl pipe, unquoted variable
eval "$USER_INPUT"
curl -fsSL https://example.com/install.sh | sh
# not flagged: no check looks for unquoted variables
rm -rf $DEPLOY_DIR# GOOD: explicit commands, quoted variables, download then inspect
allowed_commands=("list" "status" "restart")
if [[ " ${allowed_commands[*]} " =~ " ${1} " ]]; then
systemctl "$1" myservice
fi
rm -rf "$DEPLOY_DIR"
curl -fsSL https://example.com/install.sh -o install.sh
# Review install.sh
bash install.shChecks and limits
Every check, with the severity and confidence the code assigns.
| Check | Fires when | Severity | Conf. |
|---|---|---|---|
| Hardcoded secret | eight named-variable patterns; value not a placeholder, no /, long enough | High | 0.85 |
| AWS account ID | 12 digits then .dkr.ecr. | Low | 0.70 |
| curl piped to shell | curl ... | bash, sh or zsh | High | 0.90 |
| Permissive chmod | chmod 777, 666 or a+rwx | Medium | 0.85 |
| eval | eval and a $ on the same line | Medium | 0.75 |
| No set -e | no set -e, -u or -o text anywhere | Low | 0.60 |
| Echoed secret | echo then password, secret, token or key and : or = | Medium | 0.70 |
| curl -k | curl with -k or --insecure on one line | Medium | 0.80 |
Unquoted variables and predictable /tmp paths are not checked. The code checks hardcoded AWS account IDs, permissive chmod, missing error handling, echoed secrets and curl -k. Severity runs from Low to High, not High throughout. Findings carry the file path only. The "contains a slash" skip also hides genuine AWS secret keys.