How SQL Injection Happens in Shell Scripts and How to Fix It
A Preventable Critical Vulnerability
In the files/files/root/check_kuota.sh script, we discovered a critical SQL injection vulnerability that could have allowed attackers to execute arbitrary SQL commands by simply controlling a single command-line parameter. This script is a real-world example of how shell scripting—often used for system administration and database operations—can become a security risk when input validation is neglected.
Introduction: The Dangerous Pattern in check_kuota.sh
The check_kuota.sh file handles user quota checking by accepting a USERNAME parameter and executing SQL queries against a RADIUS database. However, a flaw in how this parameter was processed created a serious security vulnerability.
Here's the original vulnerable code:
#!/bin/bash
USERNAME=$1
# Koneksi database
DB_USER="radius"
DB_PASS="radius"
The vulnerability is deceptively simple but critical: the USERNAME variable (line 1, assigned from $1) is directly used in SQL queries without any validation or sanitization. Let's examine why this matters.
The Vulnerability Explained: Direct Interpolation into SQL
When a shell script accepts command-line arguments and uses them in SQL queries, every single character in that input becomes part of the SQL command. Consider what happens when the script executes a query like this (typical for quota checking):
mysql -u $DB_USER -p$DB_PASS -e "SELECT quota FROM users WHERE username='$USERNAME'"
An attacker who controls the USERNAME parameter can inject SQL syntax. For example:
./check_kuota.sh "admin' OR '1'='1"
This transforms the query into:
SELECT quota FROM users WHERE username='admin' OR '1'='1'
Now the condition '1'='1' is always true, returning all users' quotas instead of just one. But the attack can be far more severe:
./check_kuota.sh "admin'; DROP TABLE users; --"
This becomes:
SELECT quota FROM users WHERE username='admin'; DROP TABLE users; --'
The attacker has now executed multiple SQL statements, including a destructive command that deletes the users table. The -- comments out the trailing quote, making the SQL syntactically valid.
Why This Matters for Production Code:
The check_kuota.sh script likely runs with database credentials and possibly elevated privileges. An attacker exploiting this vulnerability could:
- Read sensitive data (user quotas, authentication credentials, personal information)
- Modify database records (change quotas, permissions, account statuses)
- Delete critical data
- Potentially execute commands if the database has additional privileges configured
- Perform authentication bypass by modifying user records
The vulnerability is classified as CRITICAL because the exploitation is straightforward, requires no special privileges, and the impact is complete database compromise.
The Fix: Input Validation Using Regex Whitelist
The security fix implemented in this PR adds strict input validation before any SQL operations occur:
#!/bin/bash
USERNAME=$1
# Validate USERNAME to prevent SQL injection
if [[ ! "$USERNAME" =~ ^[a-zA-Z0-9._@-]+$ ]]; then
echo "Invalid username"
exit 2
fi
# Koneksi database
DB_USER="radius"
DB_USER="radius"
DB_PASS="radius"
What changed:
Lines 5-8 introduce a regex pattern match that validates the USERNAME before it's used anywhere in the script. The pattern ^[a-zA-Z0-9._@-]+$ means:
^- Start of string[a-zA-Z0-9._@-]+- One or more characters from the set: lowercase letters, uppercase letters, digits, period, underscore, @, or hyphen$- End of string
If the USERNAME contains ANY character outside this whitelist, the validation fails, the script prints "Invalid username", and exits with code 2 (a non-zero exit indicating an error).
How this prevents the attack:
When an attacker attempts injection:
./check_kuota.sh "admin' OR '1'='1"
The regex rejects it immediately because it contains single quotes, spaces, and parentheses—none of which are in the whitelist. The script terminates before any SQL query is executed.
Why this specific pattern?
The regex pattern ^[a-zA-Z0-9._@-]+$ is carefully chosen because typical usernames in RADIUS and most authentication systems use:
- Alphanumeric characters (a-z, A-Z, 0-9)
- Period (.) for names like "john.smith"
- Underscore (_) for "john_smith"
- @ symbol for email-based usernames
- Hyphen (-) for "john-smith"
Any legitimate username will pass this validation. Any attempt to inject SQL syntax (single quotes, semicolons, comments, dashes, etc.) will be rejected.
Prevention & Best Practices
This vulnerability reveals important security principles for shell script development:
1. Always Validate Input at Entry Points
Every variable that comes from outside the script—command-line arguments, environment variables, file contents, or API responses—is untrusted. Apply validation before using it.
# Good: Validate before use
if [[ ! "$INPUT" =~ ^[a-zA-Z0-9]+$ ]]; then
echo "Invalid input"
exit 1
fi
# Bad: Using input without validation
echo $INPUT
2. Use Prepared Statements or Parameterized Queries
If your database interface supports it, use parameterized queries:
# Better approach: Use database client libraries that support parameterized queries
# Instead of shell string concatenation, use a language with proper DB bindings
mysql --user="$DB_USER" --password="$DB_PASS" -e "SELECT * FROM users WHERE username = ?" "$USERNAME"
3. Apply Principle of Least Privilege
The database user running quota checks should have only SELECT permissions on specific tables, never DELETE or DROP permissions. This limits damage even if injection succeeds:
GRANT SELECT ON radius.users TO 'check_kuota'@'localhost';
4. Use Static Analysis Tools
Tools like Semgrep can automatically detect these patterns:
semgrep --config=p/security-audit-bash check_kuota.sh
5. Log and Monitor Database Queries
Enable query logging to detect unusual patterns that might indicate injection attempts:
# In MySQL configuration
log_error_verbosity = 2
general_log = ON
general_log_file = '/var/log/mysql/queries.log'
Key Takeaways
- Never trust command-line arguments: The
$USERNAMEvariable came directly from$1without any validation, a common but critical mistake in shell scripts - Regex whitelist validation is effective for known formats: Using
^[a-zA-Z0-9._@-]+$to validate usernames before SQL use prevents injection entirely - Exit with non-zero codes on validation failure: The fix exits with code 2 when validation fails, allowing calling scripts to detect and handle the error appropriately
- SQL injection in shell scripts is easily exploitable: Unlike complex attack chains, this vulnerability requires just one command with a crafted argument—no special tools or network access needed
- Input validation should happen immediately: The regex check occurs before any database connections, ensuring failures fail-fast and safely
How Orbis AppSec Detected This
Source: The USERNAME parameter enters the script via the command-line argument $1 at line 1 of files/files/root/check_kuota.sh. This is untrusted user-controlled input.
Sink: The dangerous call site is the implicit SQL query execution that uses $USERNAME without any validation (lines that follow the parameter assignment in typical quota-checking operations).
Missing control: The original script lacked any input validation, whitelist checking, or parameterized query mechanism. The USERNAME variable was directly interpolated into SQL contexts.
CWE: CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
Fix: Added a regex pattern validation check [[ ! "$USERNAME" =~ ^[a-zA-Z0-9._@-]+$ ]] that rejects any USERNAME containing characters outside the allowed set, and exits with an error before any SQL operations.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
SQL injection in shell scripts is a preventable critical vulnerability. The check_kuota.sh fix demonstrates that robust security doesn't require complex solutions—strict input validation using a whitelist regex pattern eliminates the injection vector entirely.
When writing shell scripts that interact with databases or execute system commands, remember: validate all external input before use. This single practice prevents the vast majority of injection vulnerabilities. Combined with the principle of least privilege, static analysis tooling, and proper error handling, your shell scripts can be secure and reliable.
For teams maintaining legacy scripts or shell-based infrastructure, auditing for similar patterns should be a priority. SQL injection in shell scripts often goes overlooked because developers think of shell as a "simple" language, but the security principles are identical to any other environment.
References
- CWE-89: SQL Injection — https://cwe.mitre.org/data/definitions/89.html
- OWASP SQL Injection — https://owasp.org/www-community/attacks/SQL_Injection
- OWASP Input Validation Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html
- Bash Parameter Expansion and Security — https://www.gnu.org/software/bash/manual/html_node/Parameter-Expansion.html
- Semgrep Rule: SQL Injection in Shell — https://semgrep.dev/r?q=sql-injection
- GitHub PR — fix: the check_kuota in check_kuota.sh