
Common Web Security Attacks Every Developer Should Know
Learn about XSS, CSRF, SQL injection, DDoS and other critical security vulnerabilities that developers must understand to build secure applications.
When I first started building projects, I thought security was something I'd worry about later — maybe after I mastered React or landed a job.
I couldn't have been more wrong.
While working on a basic to-do app, a developer friend pointed out that my comment box was vulnerable to an "XSS attack." I had no idea what that meant — until he showed me how easily someone could inject malicious code through a simple input field.
That moment changed how I thought about web development.
Security isn't an advanced topic reserved for experts — it's foundational knowledge. And the truth is, almost every project I was building — forms, user auth, data displays — had hidden vulnerabilities I didn't even realize.
If you're showcasing these projects publicly, it's crucial to understand the common security attacks that target beginner code. They're not advanced hacks — just smart ways attackers exploit common mistakes.
Let's break them down.
1. Cross-Site Scripting (XSS)

XSS attacks inject malicious scripts into trusted websites
XSS happens when an attacker injects malicious scripts into web pages viewed by other users. I learned this the hard way when a simple comment section became a gateway for stolen cookies.
// Vulnerable code - DON'T DO THIS
function displayComment(comment) {
document.getElementById("comments").innerHTML += `<p>${comment}</p>`;
}
// Attacker input: <script>alert('XSS!');</script>
// Result: Script executes on other users' browsersThe Fix:
// Safe approach - escape HTML
function displayComment(comment) {
const safeComment = comment
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
document.getElementById("comments").innerHTML += `<p>${safeComment}</p>`;
}
// Or better yet, use textContent
document.getElementById("comments").textContent = comment;2. SQL Injection

SQL injection manipulates database queries through user input
This attack manipulates your database queries by injecting malicious SQL code through user inputs. It's like giving attackers a direct line to your database.
-- Vulnerable query - DON'T DO THIS
SELECT * FROM users WHERE email = '" + userInput + "' AND password = '" + password + "'
-- Attacker input: admin@example.com' OR '1'='1' --
-- Resulting query bypasses authentication:
SELECT * FROM users WHERE email = 'admin@example.com' OR '1'='1' --' AND password = ''The Fix:
// Use parameterized queries
const query = "SELECT * FROM users WHERE email = ? AND password = ?";
db.query(query, [userEmail, hashedPassword], (err, results) => {
// Handle results safely
});
// Or with modern ORMs
const user = await User.findOne({
where: {
email: userEmail,
password: hashedPassword,
},
});3. Cross-Site Request Forgery (CSRF)
CSRF tricks users into performing unwanted actions on authenticated sites
CSRF tricks users into performing actions they didn't intend to perform. The attacker leverages the user's existing authentication to perform malicious actions.
<!-- Malicious site creates this form -->
<form action="https://yourbank.com/transfer" method="POST">
<input type="hidden" name="amount" value="10000" />
<input type="hidden" name="to" value="attacker-account" />
</form>
<script>
document.forms[0].submit();
</script>The Fix:
// Implement CSRF tokens
app.use(csrf());
app.get('/transfer', (req, res) => {
res.render('transfer', { csrfToken: req.csrfToken() });
});
// In your form
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<input type="number" name="amount" required>
<button type="submit">Transfer</button>
</form>4. Insecure Direct Object Reference (IDOR)

IDOR allows unauthorized access to objects by manipulating references
IDOR occurs when applications expose direct references to internal objects without proper authorization checks.
// Vulnerable endpoint - DON'T DO THIS
app.get("/api/documents/:id", (req, res) => {
const document = db.getDocument(req.params.id);
res.json(document); // Anyone can access any document by changing the ID
});The Fix:
// Proper authorization check
app.get("/api/documents/:id", authenticateUser, (req, res) => {
const document = db.getDocument(req.params.id);
// Verify ownership or permissions
if (document.userId !== req.user.id && !req.user.isAdmin) {
return res.status(403).json({ error: "Access denied" });
}
res.json(document);
});5. Command Injection

Command injection executes arbitrary system commands through user input
This happens when user input is passed directly to system commands without proper validation.
// Dangerous code - DON'T DO THIS
const { exec } = require("child_process");
app.post("/ping", (req, res) => {
const host = req.body.host;
exec(`ping -c 4 ${host}`, (error, stdout) => {
res.send(stdout);
});
});
// Attacker input: "google.com; rm -rf /"
// Results in: ping -c 4 google.com; rm -rf /The Fix:
// Input validation and sanitization
const validator = require("validator");
app.post("/ping", (req, res) => {
const host = req.body.host;
// Validate input
if (!validator.isFQDN(host) && !validator.isIP(host)) {
return res.status(400).json({ error: "Invalid host" });
}
// Use safer alternatives or escape properly
const { spawn } = require("child_process");
const ping = spawn("ping", ["-c", "4", host]);
let output = "";
ping.stdout.on("data", (data) => {
output += data;
});
ping.on("close", () => {
res.send(output);
});
});6. Distributed Denial of Service (DDoS)

DDoS attacks overwhelm servers with massive traffic from multiple sources
DDoS attacks aren't about breaking into your system — they're about breaking it entirely. I experienced this firsthand when my side project went viral on Reddit, and my $5 server couldn't handle the legitimate traffic. Now imagine if that traffic was malicious and intentional.
A DDoS attack floods your server with so many requests that legitimate users can't access it. It's like a crowd of people blocking a store entrance — nobody can get in to shop.
// Vulnerable endpoint - no rate limiting
app.post("/api/newsletter/subscribe", async (req, res) => {
const email = req.body.email;
// Expensive operation: sends email, updates database
await sendWelcomeEmail(email);
await saveToDatabase(email);
res.json({ success: true });
});
// Attacker sends thousands of requests per second
// Server crashes under loadThe Fix:
// Implement rate limiting
const rateLimit = require("express-rate-limit");
// Basic rate limiter
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: "Too many requests from this IP, please try again later.",
standardHeaders: true,
legacyHeaders: false,
});
// Apply to all routes
app.use(limiter);
// Stricter limits for expensive operations
const strictLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 5, // Only 5 requests per minute
message: "Too many requests, please slow down.",
});
app.post("/api/newsletter/subscribe", strictLimiter, async (req, res) => {
const email = req.body.email;
await sendWelcomeEmail(email);
await saveToDatabase(email);
res.json({ success: true });
});
// Additional protection with request size limits
app.use(express.json({ limit: "10kb" })); // Prevent large payloads
// Implement timeout for long-running operations
app.use((req, res, next) => {
req.setTimeout(5000); // 5 second timeout
next();
});Additional Protection Strategies:
// Use a reverse proxy like Nginx or Cloudflare
// This is your nginx.conf
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
server {
location /api/ {
limit_req zone=mylimit burst=20 nodelay;
proxy_pass http://localhost:3000;
}
}
// Implement connection throttling
const slowDown = require("express-slow-down");
const speedLimiter = slowDown({
windowMs: 15 * 60 * 1000,
delayAfter: 50, // Allow 50 requests per 15 minutes
delayMs: 500, // Add 500ms of delay per request above 50
});
app.use("/api/", speedLimiter);Pro Tip: For production apps, use services like Cloudflare, AWS Shield, or similar CDN providers that have built-in DDoS protection. They filter malicious traffic before it ever reaches your server.
Quick Security Checklist
Before deploying any feature, I now run through this mental checklist:
- Input Validation: Are all user inputs validated and sanitized?
- Authentication: Are protected routes properly authenticated?
- Authorization: Can users only access their own data?
- Output Encoding: Are dynamic outputs properly escaped?
- HTTPS: Is all sensitive data transmitted securely?
- Dependencies: Are all third-party packages up to date?
- Rate Limiting: Are expensive endpoints protected from abuse?
Pro Tips from the Trenches
-
Think Like an Attacker: For every input field, ask "What if someone enters malicious code here?"
-
Defense in Depth: Don't rely on a single security measure. Layer your defenses.
-
Fail Securely: When something goes wrong, fail in a way that doesn't expose sensitive information.
-
Keep Learning: Security threats evolve constantly. Stay updated with OWASP Top 10.
-
Monitor Your Traffic: Set up alerts for unusual traffic patterns — it's often the first sign of an attack.
Wrapping Up
Security isn't someone else's problem - it's every developer's responsibility. These attacks are preventable with proper coding practices and awareness. The key is building security into your development process from day one, not bolting it on afterward.
Start small: validate inputs, use parameterized queries, implement proper authentication, and add rate limiting. These simple steps prevent the majority of common attacks.
Remember, the goal isn't to become a security expert overnight, but to write code that doesn't accidentally open doors for attackers. Your users' data depends on it.
Want to dive deeper into web security? Check out the OWASP Top 10 for the most critical security risks facing web applications today.
