The Importance of Web Security
Web applications are constantly exposed to attack risks. OWASP (Open Web Application Security Project) publishes the Top 10, summarizing the most critical security risks.
| Rank | Vulnerability | Description |
|---|
| 1 | Broken Access Control | Access to unauthorized resources |
| 2 | Cryptographic Failures | Weak encryption, exposed data |
| 3 | Injection | SQL, NoSQL, OS command injection |
| 4 | Insecure Design | Missing security controls in design |
| 5 | Security Misconfiguration | Default configs, exposed errors |
| 6 | Vulnerable and Outdated Components | Unpatched dependencies |
| 7 | Identification and Authentication Failures | Weak auth mechanisms |
| 8 | Software and Data Integrity Failures | Unsigned updates, CI/CD issues |
| 9 | Security Logging and Monitoring Failures | Insufficient visibility |
| 10 | Server-Side Request Forgery (SSRF) | Server-initiated malicious requests |
XSS (Cross-Site Scripting)
Types of Attacks
| Type | Description | Example |
|---|
| Reflected XSS | Script in URL reflected in response | /search?q=<script>alert('XSS')</script> reflected in HTML output |
| Stored XSS | Attack code persists in database | Comment with script → Stored in DB → Shown to other users |
| DOM-based XSS | Client-side JS inserts URL params into DOM | JavaScript reads URL and writes to innerHTML |
Defenses
function escapeHtml(text: string): string {
const map: Record<string, string> = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
return text.replace(/[&<>"']/g, (m) => map[m]);
}
<div>{userInput}</div>
<div dangerouslySetInnerHTML={{ __html: userInput }} />
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: `
default-src 'self';
script-src 'self' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com;
`.replace(/\n/g, ''),
},
];
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 3600000,
});
CSRF (Cross-Site Request Forgery)
flowchart TB
subgraph Step1["1. User logs into legitimate site"]
U1["User"] -->|"Login"| B["Bank Site<br/>(Session)"]
end
subgraph Step2["2. User visits attacker's trap site"]
U2["User"] --> A["Attacker Site"]
A -->|"Hidden form auto-submit"| Form["<form action='bank.com/transfer'><br/>to=attacker, amount=10000"]
end
subgraph Step3["3. Attack execution"]
Form -->|"Request with user's session"| B
end
Defenses
import { randomBytes } from 'crypto';
function generateCsrfToken(): string {
return randomBytes(32).toString('hex');
}
app.use((req, res, next) => {
if (!req.session.csrfToken) {
req.session.csrfToken = generateCsrfToken();
}
res.locals.csrfToken = req.session.csrfToken;
next();
});
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value={csrfToken} />
...
</form>
app.post('/transfer', (req, res) => {
if (req.body._csrf !== req.session.csrfToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
});
res.cookie('session', token, {
sameSite: 'strict',
httpOnly: true,
secure: true,
});
app.use((req, res, next) => {
const origin = req.headers.origin;
const allowedOrigins = ['https://myapp.com'];
if (req.method !== 'GET' && !allowedOrigins.includes(origin)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
});
SQL Injection
Vulnerable Code
app.get('/users', async (req, res) => {
const { name } = req.query;
const result = await db.query(`SELECT * FROM users WHERE name = '${name}'`);
res.json(result);
});
Defenses
app.get('/users', async (req, res) => {
const { name } = req.query;
const result = await db.query(
'SELECT * FROM users WHERE name = $1',
[name]
);
res.json(result);
});
const users = await prisma.user.findMany({
where: { name },
});
const users = await db.select().from(usersTable).where(eq(usersTable.name, name));
import { z } from 'zod';
const QuerySchema = z.object({
name: z.string().max(100).regex(/^[a-zA-Z0-9\s]+$/),
});
app.get('/users', async (req, res) => {
const result = QuerySchema.safeParse(req.query);
if (!result.success) {
return res.status(400).json({ error: 'Invalid input' });
}
});
Authentication Security
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 12;
async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET!;
const JWT_EXPIRES_IN = '15m';
const REFRESH_TOKEN_EXPIRES_IN = '7d';
function generateTokens(userId: string) {
const accessToken = jwt.sign({ userId }, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN,
algorithm: 'HS256',
});
const refreshToken = jwt.sign({ userId, type: 'refresh' }, JWT_SECRET, {
expiresIn: REFRESH_TOKEN_EXPIRES_IN,
algorithm: 'HS256',
});
return { accessToken, refreshToken };
}
import rateLimit from 'express-rate-limit';
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: { error: 'Too many login attempts' },
standardHeaders: true,
legacyHeaders: false,
});
app.post('/login', loginLimiter, async (req, res) => {
});
import helmet from 'helmet';
app.use(helmet());
app.use((req, res, next) => {
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
next();
});
Security Checklist
| Category | Check Item |
|---|
| Authentication | Password hashing (bcrypt/Argon2) |
| Authentication | Session fixation protection |
| Authentication | MFA support |
| Input | Validate all inputs |
| Input | Use parameterized SQL queries |
| Output | HTML escaping |
| Communication | HTTPS required |
| Cookie | HttpOnly, Secure, SameSite |
| Headers | CSP, HSTS configured |
| Dependencies | Vulnerability scanning (npm audit) |
Reference Links
← Back to list