Command injection is considered to be one of the five most dangerous injection attacks. It's equivalent to a malicious attacker using your system themselves. Imagine the damage an attacker will be able to do if they were to get access to your entire system.
As a developer, you've used the command line terminal to do literally everything—creating folders, reading files, or even deleting them. Command injection transfers all this power to the attacker. But how does that really happen? What all can an attacker do?
In this post, I'll help you understand what command injection is and how it works using an example. I'll also walk you through how you can prevent it in your React application.
What Is an Injection Attack?
Most injection attacks follow a similar pattern across all their variants. In its most primitive step, an injection attack finds a vulnerability in the application. This vulnerability provides a gateway to get unauthorized access to server files, system OS, etc. The attacker then injects some code through this gateway to steal data, modify system files, or execute shell commands.
Based on the type of injection attack, the code is injected in different ways. If it's a client-side vulnerability, the easiest way for an attacker to inject code is through JavaScript. In this case, the attacker injects a script that runs on the user's browser. If you're curious as to how script injection works, have a look at my other blog where I talk about XSS attacks in React in detail.
On the other hand, if it's the server, the attacker could inject some shell commands. We know how powerful shell commands are. They can interact directly with your system-level APIs.
What Exactly Is a Command Injection Attack?
A command injection attack is more lethal because it gives the attacker more privileges than a regular injection attack. Earlier, I talked about how attackers can inject a malicious script on the client side. However, the script can only execute some JavaScript. The extent to which it can hamper your application is largely influenced by what JavaScript can do.
In other words, injecting code or a script often becomes limited to the language. However, that's not the case with command injection.
A typical command injection attack allows the attacker to execute shell commands on your server.
This gives the attacker complete control over your system. Consequently, the attacker can read your environment secrets and other configurational files. Not only this, but the attacker can also modify or delete other files on your system.
Example of a Command Injection Attack
Typical command injection attacks happen directly on the server, but they may also be triggered from the client side. Let's assume you have a React app on the front end and a NodeJS server on the back end.
Create a Back-End Server
To set up the latter, run the following command:
cd command-injection-server && npm init -y && npm i express
Let's assume that your back end receives the name of a text file stored locally on your server. This text file stores the version of your server. You need to validate if your back end and front end are running on the same version. So, you make an HTTP request to an endpoint. You send the version file as a query parameter from the front end to an endpoint. This endpoint checks if that version file exists on the server. If it does, you send back the contents of the file. Otherwise, you throw an error.
Consider the following code that does all of the above:
const express=require('express');
const app=express();
const PORT=8080;
const exec = require('child_process').exec;
app.get('/', (req, res) => {
res.set('Access-Control-Allow-Origin', '*');
const appVersionFile= req.query.versionFile;
const command = `type ${appVersionFile}`;
exec(command, (err, output) => {
if (err) {
res.status(500).send(err);
return;
}
res.send({version:output});
});
});
app.listen(PORT,()=>console.log(`server started on port ${PORT}`))
To demonstrate, let's make a v1.txt file in the root directory of your project. Add the following content inside that text file:
App Version 1
Your project structure should look like this:
Version API Back-End Project Structure.
If you now make a request to http://localhost:8080/?versionFile=v1.txt endpoint, you'll get back the following response:
Version API Response.
Consume Version API on Front End
You saw the above response of the version check API from the server. However, you need to make a request to the above endpoint from your React app. First, let's create an empty React app by running:
npx create-react-app command-injection-client
Rewrite your App.js file to the following:
import {useEffect} from 'react';
import './App.css';
function App() {
const getAppVersion=async()=>{
const response=await fetch('http://localhost:8080/?versionFile=v1.txt',{mode:'cors'});
const data=await response.json();
console.log(data);
}
useEffect(()=>{
getAppVersion();
},[])
return (
<div className="App">
<h1>Command Injection Attack with ReactJS+NodeJS 🚀</h1>
</div>
);
}
export default App;
In the above code, I simply invoke a method that makes an HTTP GET request to the server at the http://localhost:8080/?versionFile=v1.txt endpoint. I call this function inside the useEffect so that it's fired as soon as the page loads. If you check the console, you'll get back the app version in response as shown:
Version API Front End.
Command Injection Vulnerability
Until now, it may seem as if everything is fine. There's a server that serves an endpoint for version check and your React app makes a request to it. However, the endpoint exposes a command injection vulnerability. Let's see how.
The front end hits the version endpoint with a query parameter that executes a shell command on the server. The query parameter is the file name that contains the version of the app. It's extracted by your server and is directly taken to execute a command. An attacker could easily infiltrate this request and send some malicious commands that can be executed on the server.
Let's say we also had a secrets folder that contains all the sensitive configurational credentials of our project. An attacker could make a request like this from the front end:
const response=await fetch('http://localhost:8080/?versionFile=v1.txt&&cd%20secrets',{mode:'cors'});
which would then execute the following command on the server:
type v1.txt && cd secrets
Now the attacker can access your secrets folder! This is just a simple example, but there are a ton of dangerous commands an attacker can execute. Here's a detailed guide that tells you all the realistic attacks the attacker can commit using command injection once your system is compromised. For now, let's move ahead and see how we can fix this problem.
Prevent Command Injection Attack
There are several methods, best practices, and coding guidelines you can follow to prevent a command injection attack on your application. Let's have a look at some of the methods below, what they do and how they combat command injection.
Refactor Your API
If you head back to the back-end code, the following lines of code are the bottlenecks for the command injection vulnerability in your system:
const appVersionFile= req.query.versionFile;
const command = `type ${appVersionFile}`;
We're directly getting the file name as a query parameter in the API. We're then using this file name directly in the command. Thus, any infiltration with the query parameter is directly going to affect the shell command executed on the server. Besides, it doesn't make a lot of sense to send a hardcoded file name as a query parameter from the front end.
Let's refactor the above lines of code to the following:
const appVersion= req.query.version;
const versionFile=`v${appVersion}.txt`;
const command = `type ${versionFile}`;
We have changed the query parameter to be only the version number that we need to check. This is because we don't really need an entire file name as a query parameter in the API. We then use the version number to dynamically generate a version file name. Finally, we use that filename to execute a command. If you now make the same request as earlier, you'll get an error with the following:
{
killed: false,
code: 1,
signal: null,
cmd: "type vundefined.txt"
}
Similarly, if the attacker tries to inject a command in your server through your React app, they won't be able to do so as the API would throw an exception.
Version API Refactored Response.
Hence, the attacker won't be able to run any lethal shell commands.
Use More Airtight Functions for Executing Shell Commands
We use the exec function to execute the shell commands. According to NodeJS official docs, this function takes in a command that runs it as it is, "with space-separated arguments." Instead, you can use a more airtight function that disallows your server to run arbitrary commands.
The execFile function takes in a file that contains some shell commands. Additionally, it also takes some arguments to run those commands. It's more secure as now you don't generate commands on the fly. Instead, you store them inside a bash file and can send some arguments specific to the command you want to execute. You can read more about this function here.
Validate Input
I can't emphasize enough how important it is to validate inputs from the front end. In this scenario, you can validate the query parameters before sending them to the server. Have a look at the following code:
const validateQueryParam=(queryParam)=>{
const infiltratedParams=queryParam.split('&&');
console.log(infiltratedParams)
if(infiltratedParams.length>1) return false;
else return true;
}
const getAppVersion=async()=>{
const queryParam="versionFile=v1.txt&&cd%20secrets";
const isValidQueryParam=validateQueryParam(queryParam);
if(!isValidQueryParam){
alert('invalid query params');
return;
}
const response=await fetch(`http://localhost:8080/?${queryParam}`,{mode:'cors'});
const data=await response.json();
console.log(data);
}
The above code validates the query parameters on the front end before sending them in the request. The validateQueryParam function checks if the query parameters are infiltrated. If this function returns true, the front end blocks the API request and throws an alert.
Command Injection Prevention From Front End
You can also validate the query parameters against a more robust regular expression.
Conclusion
I hope this post helped to simplify command injection for you. Make sure you validate all inputs on the front end. This safeguards your application from several malicious attacks, like command injection and SQL injection.
However, you can't completely rely on input validation from the client-side to prevent such an attack. The most foolproof protection from such an attack must be implemented on the server-side. Remember to use secure functions when running shell commands. Finally, routinely refactor your code to detect potential bottlenecks.
This post was written by Siddhant Varma. Siddhant is a full stack JavaScript developer with expertise in frontend engineering. He’s worked with scaling multiple startups in India and has experience building products in the Ed-Tech and healthcare industries. Siddhant has a passion for teaching and a knack for writing. He's also taught programming to many graduates, helping them become better future developers.