VNX-BASH-003: Failure ignored in a script without set -e / set -u
In a shell script that does not enable errexit, reports an unchecked cd followed by a relative-path delete, an integrity check whose result nothing reads, and (without nounset) rm -r of a path built from a variable that can be empty.
| Field | Value |
|---|---|
| Rule ID | VNX-BASH-003 |
| Severity | Medium |
| SARIF level | warning |
| Kind | sast |
| Languages | Shell |
| CWE | CWE-755 |
| CWSS vector | CWSS:1.0/TI:M/AP:RU/AL:S/IC:N/FC:LT/RP:RU/RL:S/AV:L/AS:N/IN:A/SC:A/BI:M/DI:H/EX:M/EC:N/P:C |
| Tags | bash error-handling set-e best-practice |
| Source | internal/sast/rules/vnx-bash-003.rego |
Overview
In an executable shell script (one with a sh/bash/dash/ksh/zsh shebang) that does not enable errexit (set -e, set -o errexit, a -e shebang flag or an ERR trap), this rule reports the three places where a silently ignored failure does damage: a bare cd or pushd that nothing checks (no || exit, &&, if or $? test) followed within ten lines by a delete with a relative path (rm ./*, rm -rf build, find . -delete, git clean -f, git reset --hard); an integrity check (sha256sum -c, gpg --verify, cosign verify, minisign -V, openssl dgst -verify) run as a bare statement whose result nothing reads; and, when nounset is also off, rm -r "$VAR/..." where VAR can be empty (an unchecked positional parameter, an environment variable the script never sets, or a command-substitution result) with no ${VAR:?}, default or -z/-n test. Not reported: scripts that enable errexit (and nounset, for the rm case), checked cd and checks, cd followed by non-destructive commands or absolute paths, a check that is the last command or a function’s return value, sourced libraries without a shebang, test paths and .bats files. The mere absence of set -euo pipefail is not reported.
Without set -e, a script continues executing even when a command fails with a non-zero exit code. A failed cp, curl, or database migration command will be silently ignored and subsequent steps will operate on incomplete or incorrect state. Without set -u, any typo in a variable name expands to the empty string rather than producing an error, so rm -rf "$MY_DIR/" becomes rm -rf "/" if MY_DIR is unset. Without pipefail, a failing command early in a pipeline (curl ... | jq ...) is invisible because the pipeline exit code is taken from the last command only.
Why This Matters
Silent failure in shell scripts is a common root cause of data loss, partial deployments, and security configuration mistakes. A deployment script that continues past a failed gpg --verify check may install a tampered artifact. A backup script that continues past a failed tar invocation may overwrite the previous backup with an empty file, believing the operation succeeded.
From a security perspective, scripts that silently swallow errors are particularly dangerous in CI/CD pipelines, bootstrap scripts, and container entrypoints. An attacker who can cause one step to fail, for example by removing a file, exhausting disk space, or providing malformed input, may be able to force the script into an unexpected code path where later security checks are skipped entirely, or where subsequent commands operate on attacker-controlled fallback values.
The pipefail option is especially important when scripts parse the output of external commands through pipes. Without it, a command injection attempt that causes the first stage of a pipeline to fail goes undetected while the rest of the pipeline continues to execute with potentially attacker-influenced output from a partial run.
What Gets Flagged
#!/bin/bash
cd "$BUILD_DIR" # FLAGGED: if cd fails, the rm runs here
rm -rf ./*
sha256sum -c app.tar.gz.sha256 # FLAGGED: a failed check is ignored
tar -xzf app.tar.gz -C /opt/app
rm -rf "${CACHE_DIR}/"* # FLAGGED: CACHE_DIR unset means rm -rf /*
Not flagged:
#!/bin/bash
set -euo pipefail
cd "$BUILD_DIR"
rm -rf ./*
cd "$BUILD_DIR" || exit 1
rm -rf "${CACHE_DIR:?}/"*
Remediation
- Add
set -euo pipefailimmediately after the shebang line in every Bash script. - For POSIX
shscripts (which lackpipefail), useset -euas a minimum. - When you intentionally need to allow a command to fail, use the
|| trueidiom or a conditional rather than omitting the flag globally. - For commands where failure is expected, capture the exit code explicitly:
some_command || exit_code=$?.
# SAFE: error handling enabled immediately after shebang
#!/usr/bin/env bash
set -euo pipefail
DEPLOY_DIR="/opt/app"
# Intentional fallback where failure is acceptable
mkdir -p "$DEPLOY_DIR" || true
cp -r ./dist "$DEPLOY_DIR"
chown -R www-data "$DEPLOY_DIR"
systemctl restart app
References
- CWE-755: Improper Handling of Exceptional Conditions
- ShellCheck SC2078 – set -e / errexit guidance
- BashFAQ/105 – Why doesn’t set -e (or set -o errexit) do what I expected?
- OWASP Testing Guide – Testing for OS Command Injection
- Google Shell Style Guide – Error Handling
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-bash-003
// nosec vnx-bash-003
Or record an ignore rule: vulnetix ignore add --rule vnx-bash-003 --reason "...". See Suppressing findings.