Injection Attacks Beyond XSS: SQL and Command Injection
Injection attacks extend far beyond XSS. SQL injection, command injection, and LDAP injection allow attackers to manipulate backend systems when React apps send unsanitized user input to servers. While React itself cannot prevent these attacks (they occur on the backend), React developers are responsible for safe API communication—validating inputs before sending and handling responses securely. In 2026, injection attacks (all types combined) remain the #1 OWASP vulnerability, affecting 47% of web applications, and poor client-server communication is a primary vector.
What Is SQL Injection?
SQL injection occurs when an attacker injects SQL commands into input fields, and the backend constructs a SQL query by concatenating the input. The injected SQL executes with database permissions, allowing data theft, modification, or deletion.
Definition: SQL injection is a code injection attack where malicious SQL fragments are inserted into query strings, allowing attackers to execute unintended database commands with the application's privileges.
Classic SQL Injection Example
A backend endpoint accepts a username and constructs a query:
// VULNERABLE Backend Code (do not use this pattern)
app.post('/login', (req, res) => {
const { username, password } = req.body;
// UNSAFE: concatenating user input into SQL
const query = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;
// Attacker input: username = "admin' --"
// Resulting query: SELECT * FROM users WHERE username = 'admin' --' AND password = '...'
// The -- comments out the password check, allowing login without the correct password!
db.query(query, (err, rows) => {
if (rows.length > 0) res.send('Logged in');
else res.send('Failed');
});
});
An attacker who enters username = "admin' OR '1'='1" transforms the query to:
SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = '...'
This returns all users because '1'='1' is always true.
Command Injection (Shell Injection)
When a backend spawns a shell process and passes user input as a command argument, an attacker can inject shell metacharacters to execute arbitrary commands.
Definition: Command injection occurs when unsanitized user input is passed to a system shell, allowing attackers to execute arbitrary OS commands with the application's privileges.
Command Injection Example
// VULNERABLE Backend Code (do not use)
const { exec } = require('child_process');
app.post('/resize-image', (req, res) => {
const { filename } = req.body;
// UNSAFE: passing user input to shell
const cmd = `convert ${filename} -resize 100x100 output.png`;
exec(cmd, (err, stdout) => {
if (err) res.status(500).send('Resize failed');
else res.send('Resized');
});
});
// Attacker input: filename = "image.jpg; rm -rf /data"
// Resulting command: convert image.jpg; rm -rf /data -resize 100x100 output.png
// The semicolon separates commands; rm -rf /data executes and deletes the /data directory!
LDAP Injection
When a backend constructs LDAP queries (used for authentication against Active Directory or OpenLDAP) from user input, injection is possible:
// VULNERABLE: LDAP query with user input
const ldap = require('ldapjs');
const client = ldap.createClient({ url: 'ldap://ldap.example.com' });
app.post('/authenticate', (req, res) => {
const { username } = req.body;
// UNSAFE: concatenating user input into LDAP filter
const filter = `(uid=${username})`;
// Attacker input: username = "admin)(|(uid=*"
// Resulting filter: (uid=admin)(|(uid=*)) — this bypasses authentication
client.search('dc=example,dc=com', { filter }, (err, res) => {
// ...
});
});
How React Apps Expose These Vulnerabilities
React itself does not execute SQL or shell commands, but React developers often make mistakes in client-server communication:
1. Trusting Client-Side Validation
// UNSAFE: Only client-side validation
function LoginForm() {
const [username, setUsername] = useState('');
const handleLogin = async () => {
// Client-side check (easily bypassed)
if (!username.includes("'") && username.length < 20) {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username }),
});
}
};
return <input value={username} onChange={(e) => setUsername(e.target.value)} />;
}
// An attacker can bypass client-side validation by:
// 1. Disabling JavaScript in the browser
// 2. Using curl or another HTTP client directly
// 3. Modifying the compiled React bundle
Client-side validation is UX feedback only; it does not prevent attacks.
2. Sending Unsanitized Data to the Backend
// UNSAFE: Sending raw user input to the backend
function SearchUsers({ query }) {
const handleSearch = async () => {
const res = await fetch(`/api/search?q=${query}`); // Query is URL-encoded but still raw
const results = await res.json();
// Backend might use query unsafely
};
return <input onChange={(e) => handleSearch(e.target.value)} />;
}
Even though React URL-encodes the query in the URL, the backend receives the decoded value. If the backend concatenates this into a SQL query, injection is possible.
3. Not Validating API Responses
// UNSAFE: Trusting an API response and rendering it with dangerouslySetInnerHTML
function UserProfile({ userId }) {
const [profile, setProfile] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => setProfile(data));
}, [userId]);
if (!profile) return <div>Loading...</div>;
// UNSAFE: If the backend was compromised or a MITM attack occurred,
// profile.bio might contain malicious HTML
return <div dangerouslySetInnerHTML={{ __html: profile.bio }} />;
}
If an attacker compromises the backend database or intercepts the HTTP response, they can inject malicious HTML into the API response.
Prevention: Backend-Side Measures
React developers cannot fix injection vulnerabilities alone; backend developers must implement these protections:
1. Parameterized Queries (Prepared Statements)
Use query parameters, not string concatenation:
// SAFE: Using parameterized queries
const mysql = require('mysql');
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'password',
database: 'mydb',
});
app.post('/login', (req, res) => {
const { username, password } = req.body;
// SAFE: Using placeholders (?) for parameters
const query = 'SELECT * FROM users WHERE username = ? AND password = ?';
connection.query(query, [username, password], (err, rows) => {
// ...
});
});
// The database driver handles escaping; user input cannot become SQL code.
2. Avoid Executing Shell Commands
Never spawn a shell with user input. Use language-specific APIs:
// UNSAFE
const { exec } = require('child_process');
exec(`convert ${filename} -resize 100x100 output.png`);
// SAFE: Use an API-based image processing library (e.g., Sharp)
const sharp = require('sharp');
sharp(filename)
.resize(100, 100)
.toFile('output.png', (err, info) => {
// ...
});
Sharp is a Node.js image processing library that never shells out; it's safer.
3. Input Validation on the Backend
Validate type, length, and format:
// SAFE: Backend validation
app.post('/login', (req, res) => {
const { username, password } = req.body;
// Validate type and length
if (typeof username !== 'string' || username.length > 50) {
res.status(400).send('Invalid input');
return;
}
// Validate format (alphanumeric + underscore)
if (!/^[a-zA-Z0-9_]+$/.test(username)) {
res.status(400).send('Invalid username');
return;
}
// Use parameterized query
const query = 'SELECT * FROM users WHERE username = ?';
connection.query(query, [username], (err, rows) => {
// ...
});
});
Prevention: React Developer Responsibilities
While you cannot prevent SQL injection in the backend, you can follow these practices in your React app:
1. Validate Input Locally (for UX, not Security)
function SearchForm() {
const [query, setQuery] = useState('');
const [error, setError] = useState('');
const handleSearch = async () => {
// Local validation for immediate feedback
if (query.length === 0) {
setError('Search query cannot be empty');
return;
}
if (query.length > 100) {
setError('Search query too long');
return;
}
// Send to backend (which performs its own validation)
const res = await fetch('/api/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query }),
});
const results = await res.json();
// Handle results
};
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
{error && <span className="error">{error}</span>}
<button onClick={handleSearch}>Search</button>
</div>
);
}
2. Validate and Sanitize API Responses
function UserCard({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then((res) => {
if (!res.ok) throw new Error('API error');
return res.json();
})
.then((data) => {
// Validate response structure
if (typeof data.name !== 'string' || typeof data.bio !== 'string') {
throw new Error('Invalid response');
}
setUser(data);
})
.catch((err) => console.error(err));
}, [userId]);
if (!user) return <div>Loading...</div>;
// SAFE: Display name as text (React escapes it)
// For bio, use DOMPurify if HTML is expected
return (
<div>
<h2>{user.name}</h2>
<p>{user.bio}</p>
</div>
);
}
3. Use HTTPS and Validate Certificates
Always use HTTPS to prevent MITM attacks on API responses:
// SAFE: All API calls use HTTPS
fetch('https://api.example.com/data') // ✓
// NOT: fetch('http://api.example.com/data') // ✗
Comparison: Injection Attack Prevention
| Attack | Prevention | React's Role |
|---|---|---|
| SQL Injection | Parameterized queries on backend | Validate input for UX; do not trust client-side validation |
| Command Injection | Avoid shell; use language APIs | Do not construct command strings; validate input |
| LDAP Injection | Parameterized LDAP filters | Validate input before sending to backend |
| XSS (via API response) | Sanitize backend output; validate on frontend | Use DOMPurify if HTML is expected; always escape by default |
Data point: According to a 2026 Gartner report, 62% of injection vulnerabilities in web apps stem from insufficient server-side validation, not client-side failures.
Key Takeaways
- SQL injection, command injection, and LDAP injection occur on the backend when unsanitized user input is concatenated into queries or commands.
- React cannot prevent these attacks (they happen server-side), but React developers are responsible for safe API communication.
- Client-side validation is UX feedback only; attackers can bypass it. Always validate on the backend.
- Parameterized queries are the gold standard for SQL injection prevention; they separate code from data.
- Never spawn shell processes with user input; use language-specific APIs or libraries.
- Validate API responses in React; do not trust the backend implicitly, especially for security-sensitive data.
Frequently Asked Questions
Can I prevent SQL injection by escaping user input with backslashes?
No. Escaping is context-dependent and error-prone. Different databases have different escape rules. Parameterized queries are the only reliable defense because the database driver handles escaping and ensures user input cannot become code. Never use string concatenation.
Is NoSQL immune to injection attacks?
No. NoSQL injection is possible if you construct queries from user input. For example, in MongoDB: db.collection.find({username: req.body.username}) where req.body.username = {$ne: null} can bypass authentication. Always use parameterized queries or ORM libraries, even with NoSQL.
Should I validate input on the client side at all?
Yes, but only for UX. Client-side validation provides immediate feedback and reduces unnecessary server requests. However, treat it as decoration, not security. Always validate again on the backend because client-side checks are trivial for an attacker to bypass.
What if my backend doesn't support parameterized queries?
Parameterized queries are supported by all modern database libraries (MySQL, PostgreSQL, MongoDB with ORM). If your backend uses an old library, upgrade it. If upgrade is impossible, use an ORM (Sequelize, TypeORM) which handles parameterization internally.
How do I know if my backend is vulnerable?
Code review: search for SQL query strings that include variables without parameterization. Look for query = "SELECT * FROM users WHERE id = " + userId (vulnerable) vs. query("SELECT * FROM users WHERE id = ?", userId) (safe). Use SAST tools like SonarQube or Checkmarx to automate detection.