Find the RCE in the Flask nslookup wrapper
This Flask view runs nslookup on a domain taken from the form, using a shell. Identify the vulnerability and how an attacker reaches command execution, then describe the fix.
Constraints:
domaincomes straight fromrequest.formand is attacker-controlled- the command is run with
shell=True
domain = request.form["domain"]
output = subprocess.check_output(f"nslookup {domain}", shell=True)
Diagnose the cause.
OS command injection — domain goes into a shell string with shell=True, so shell metacharacters run arbitrary commands (RCE). Fix: shell=False with an argument list, keeping input one argument, plus hostname validation.
- ✗Treating it as a DoS or SSRF problem instead of command injection
- ✗Thinking a blacklist of dangerous characters is safer than an argument list and shell=False
- ✗Assuming shell=True is required to run an external command
- →Why does shell=False with an argument list eliminate the injection at its root?
- →Why is a blacklist of special characters an unreliable defense here?
The vulnerability
domain is attacker-controlled and interpolated into a command string run through a shell:
output = subprocess.check_output(f"nslookup {domain}", shell=True)
With shell=True the interpreter parses metacharacters (|, ;, &&, $()). Any such character in the input breaks the command/data boundary: part of the string stops being a domain name and is treated as a separate command — this is OS command injection (RCE). The code smell: a command string built by concatenation and handed to a shell.
The fix
Avoid the shell; pass arguments as a list so the input becomes a single argument:
import re, subprocess
domain = request.form["domain"]
if not re.fullmatch(r"[a-zA-Z0-9.-]+", domain):
abort(400)
output = subprocess.check_output(["nslookup", domain], shell=False)
✅ shell=False + an argument list removes the injection at its root: metacharacters are no longer interpreted by a shell. Validate domain against a strict pattern as well. ⚠️ A blacklist of dangerous characters is unreliable — a bypass always exists.