DEV Community

Cover image for Cybersecurity Best Practices for Developers: A Practical Guide with Code Examples
Abishek Dongol
Abishek Dongol

Posted on

Cybersecurity Best Practices for Developers: A Practical Guide with Code Examples

Security bugs are expensive. A single SQL injection can expose millions of user records. An XSS vulnerability can compromise user sessions. Let's skip the theory and dive into practical, actionable security practices with real code examples you can use today.

1. Authentication: Don't Roll Your Own Crypto

❌ Bad: Plain Text Passwords

# NEVER DO THIS
def create_user(username, password):
    db.execute("INSERT INTO users (username, password) VALUES (?, ?)", 
               username, password)
Enter fullscreen mode Exit fullscreen mode

✅ Good: Hashed Passwords with bcrypt

import bcrypt

def create_user(username, password):
    # Generate salt and hash password
    salt = bcrypt.gensalt(rounds=12)
    hashed = bcrypt.hashpw(password.encode('utf-8'), salt)

    db.execute("INSERT INTO users (username, password_hash) VALUES (?, ?)", 
               username, hashed)

def verify_password(username, password):
    user = db.execute("SELECT password_hash FROM users WHERE username = ?", 
                      username).fetchone()

    if user and bcrypt.checkpw(password.encode('utf-8'), user['password_hash']):
        return True
    return False
Enter fullscreen mode Exit fullscreen mode

Node.js Example:

const bcrypt = require('bcrypt');

async function createUser(username, password) {
    const saltRounds = 12;
    const hashedPassword = await bcrypt.hash(password, saltRounds);

    await db.query(
        'INSERT INTO users (username, password_hash) VALUES ($1, $2)',
        [username, hashedPassword]
    );
}

async function verifyPassword(username, password) {
    const result = await db.query(
        'SELECT password_hash FROM users WHERE username = $1',
        [username]
    );

    if (result.rows.length === 0) return false;

    return await bcrypt.compare(password, result.rows[0].password_hash);
}
Enter fullscreen mode Exit fullscreen mode

2. SQL Injection: The Classic Vulnerability

❌ Bad: String Concatenation

# VULNERABLE TO SQL INJECTION
def get_user(username):
    query = "SELECT * FROM users WHERE username = '" + username + "'"
    return db.execute(query).fetchone()

# Attacker inputs: admin' OR '1'='1
# Resulting query: SELECT * FROM users WHERE username = 'admin' OR '1'='1'
# Returns all users!
Enter fullscreen mode Exit fullscreen mode

✅ Good: Parameterized Queries

def get_user(username):
    query = "SELECT * FROM users WHERE username = ?"
    return db.execute(query, (username,)).fetchone()

# Even with malicious input, it's treated as a literal string
Enter fullscreen mode Exit fullscreen mode

Node.js with PostgreSQL:

// ❌ VULNERABLE
async function getUser(username) {
    const query = `SELECT * FROM users WHERE username = '${username}'`;
    return await db.query(query);
}

// ✅ SAFE - Parameterized query
async function getUser(username) {
    const query = 'SELECT * FROM users WHERE username = $1';
    return await db.query(query, [username]);
}
Enter fullscreen mode Exit fullscreen mode

ORM Examples (Even Safer):

# Using SQLAlchemy
from sqlalchemy import select

def get_user(username):
    stmt = select(User).where(User.username == username)
    return session.execute(stmt).scalar_one_or_none()
Enter fullscreen mode Exit fullscreen mode
// Using Sequelize
async function getUser(username) {
    return await User.findOne({
        where: { username: username }
    });
}
Enter fullscreen mode Exit fullscreen mode

3. XSS Prevention: Escape User Input

❌ Bad: Direct HTML Rendering

// VULNERABLE TO XSS
function displayComment(comment) {
    document.getElementById('comments').innerHTML += `
        <div class="comment">
            <p>${comment.text}</p>
            <span>By: ${comment.author}</span>
        </div>
    `;
}

// If comment.text = '<script>alert(document.cookie)</script>'
// The script executes!
Enter fullscreen mode Exit fullscreen mode

✅ Good: Sanitize and Escape

function escapeHtml(unsafe) {
    return unsafe
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;");
}

function displayComment(comment) {
    const div = document.createElement('div');
    div.className = 'comment';

    const p = document.createElement('p');
    p.textContent = comment.text; // textContent auto-escapes

    const span = document.createElement('span');
    span.textContent = `By: ${comment.author}`;

    div.appendChild(p);
    div.appendChild(span);
    document.getElementById('comments').appendChild(div);
}
Enter fullscreen mode Exit fullscreen mode

React (Auto-Escapes by Default):

// ✅ Safe by default
function Comment({ comment }) {
    return (
        <div className="comment">
            <p>{comment.text}</p>
            <span>By: {comment.author}</span>
        </div>
    );
}

// ❌ Dangerous - only use when absolutely necessary
function RawComment({ htmlContent }) {
    return (
        <div dangerouslySetInnerHTML={{ __html: htmlContent }} />
    );
}
Enter fullscreen mode Exit fullscreen mode

Backend Sanitization (Python):

import bleach

ALLOWED_TAGS = ['p', 'br', 'strong', 'em', 'a']
ALLOWED_ATTRIBUTES = {'a': ['href', 'title']}

def sanitize_html(dirty_html):
    return bleach.clean(
        dirty_html,
        tags=ALLOWED_TAGS,
        attributes=ALLOWED_ATTRIBUTES,
        strip=True
    )

def create_post(title, content):
    clean_content = sanitize_html(content)
    db.execute(
        "INSERT INTO posts (title, content) VALUES (?, ?)",
        (title, clean_content)
    )
Enter fullscreen mode Exit fullscreen mode

4. Secure API Authentication with JWT

✅ Proper JWT Implementation:

const jwt = require('jsonwebtoken');
const crypto = require('crypto');

// Store this in environment variables, NOT in code
const JWT_SECRET = process.env.JWT_SECRET || crypto.randomBytes(64).toString('hex');
const JWT_EXPIRY = '1h';

function generateToken(userId) {
    return jwt.sign(
        { userId: userId },
        JWT_SECRET,
        { 
            expiresIn: JWT_EXPIRY,
            algorithm: 'HS256'
        }
    );
}

function verifyToken(token) {
    try {
        return jwt.verify(token, JWT_SECRET, { algorithms: ['HS256'] });
    } catch (error) {
        return null;
    }
}

// Middleware for protected routes
function authenticateToken(req, res, next) {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN

    if (!token) {
        return res.status(401).json({ error: 'Access token required' });
    }

    const decoded = verifyToken(token);
    if (!decoded) {
        return res.status(403).json({ error: 'Invalid or expired token' });
    }

    req.userId = decoded.userId;
    next();
}

// Usage
app.post('/api/login', async (req, res) => {
    const { username, password } = req.body;

    const user = await verifyUser(username, password);
    if (!user) {
        return res.status(401).json({ error: 'Invalid credentials' });
    }

    const token = generateToken(user.id);
    res.json({ token });
});

app.get('/api/protected', authenticateToken, (req, res) => {
    res.json({ message: 'Access granted', userId: req.userId });
});
Enter fullscreen mode Exit fullscreen mode

5. Secure Session Management

✅ Express.js Secure Sessions:

const session = require('express-session');
const RedisStore = require('connect-redis').default;
const redis = require('redis');

const redisClient = redis.createClient();

app.use(session({
    store: new RedisStore({ client: redisClient }),
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: {
        secure: true,        // Only send over HTTPS
        httpOnly: true,      // Not accessible via JavaScript
        maxAge: 1000 * 60 * 60, // 1 hour
        sameSite: 'strict'   // CSRF protection
    }
}));

// Login handler
app.post('/login', async (req, res) => {
    const user = await verifyUser(req.body.username, req.body.password);

    if (user) {
        // Regenerate session ID to prevent session fixation
        req.session.regenerate((err) => {
            if (err) return res.status(500).json({ error: 'Login failed' });

            req.session.userId = user.id;
            req.session.username = user.username;
            res.json({ success: true });
        });
    } else {
        res.status(401).json({ error: 'Invalid credentials' });
    }
});

// Logout handler
app.post('/logout', (req, res) => {
    req.session.destroy((err) => {
        if (err) return res.status(500).json({ error: 'Logout failed' });
        res.clearCookie('connect.sid');
        res.json({ success: true });
    });
});
Enter fullscreen mode Exit fullscreen mode

6. Input Validation: Never Trust User Input

✅ Comprehensive Validation (Node.js with express-validator):

const { body, validationResult } = require('express-validator');

app.post('/api/users',
    // Validation rules
    body('email')
        .isEmail()
        .normalizeEmail()
        .withMessage('Invalid email address'),
    body('username')
        .isLength({ min: 3, max: 20 })
        .matches(/^[a-zA-Z0-9_]+$/)
        .withMessage('Username must be 3-20 alphanumeric characters'),
    body('age')
        .optional()
        .isInt({ min: 13, max: 120 })
        .withMessage('Age must be between 13 and 120'),
    body('website')
        .optional()
        .isURL()
        .withMessage('Invalid URL'),

    // Handler
    async (req, res) => {
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
            return res.status(400).json({ errors: errors.array() });
        }

        // Safe to use validated data
        const { email, username, age, website } = req.body;
        // ... create user
    }
);
Enter fullscreen mode Exit fullscreen mode

Python with Pydantic:

from pydantic import BaseModel, EmailStr, HttpUrl, validator
from fastapi import FastAPI, HTTPException

app = FastAPI()

class UserCreate(BaseModel):
    email: EmailStr
    username: str
    age: int | None = None
    website: HttpUrl | None = None

    @validator('username')
    def validate_username(cls, v):
        if not 3 <= len(v) <= 20:
            raise ValueError('Username must be 3-20 characters')
        if not v.replace('_', '').isalnum():
            raise ValueError('Username must be alphanumeric')
        return v

    @validator('age')
    def validate_age(cls, v):
        if v is not None and not 13 <= v <= 120:
            raise ValueError('Age must be between 13 and 120')
        return v

@app.post("/api/users")
async def create_user(user: UserCreate):
    # Data is automatically validated
    # ... create user
    return {"message": "User created", "username": user.username}
Enter fullscreen mode Exit fullscreen mode

7. CSRF Protection

✅ CSRF Tokens (Express):

const csrf = require('csurf');

// Setup CSRF protection
const csrfProtection = csrf({ cookie: true });

app.use(cookieParser());

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

// Validate CSRF token on POST
app.post('/process', csrfProtection, (req, res) => {
    // If we get here, CSRF token is valid
    res.json({ success: true });
});
Enter fullscreen mode Exit fullscreen mode

HTML Form:

<form action="/process" method="POST">
    <input type="hidden" name="_csrf" value="{{ csrfToken }}">
    <input type="text" name="data">
    <button type="submit">Submit</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Fetch API with CSRF:

async function submitForm(data) {
    const csrfToken = document.querySelector('[name=_csrf]').value;

    const response = await fetch('/process', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'CSRF-Token': csrfToken
        },
        body: JSON.stringify(data)
    });

    return response.json();
}
Enter fullscreen mode Exit fullscreen mode

8. Secure File Uploads

✅ Safe File Upload Handler:

const multer = require('multer');
const path = require('path');
const crypto = require('crypto');

// Allowed file types
const ALLOWED_TYPES = {
    'image/jpeg': 'jpg',
    'image/png': 'png',
    'image/gif': 'gif',
    'application/pdf': 'pdf'
};

const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB

const storage = multer.diskStorage({
    destination: (req, file, cb) => {
        cb(null, 'uploads/');
    },
    filename: (req, file, cb) => {
        // Generate random filename to prevent path traversal
        const randomName = crypto.randomBytes(16).toString('hex');
        const ext = ALLOWED_TYPES[file.mimetype];
        cb(null, `${randomName}.${ext}`);
    }
});

const upload = multer({
    storage: storage,
    limits: { fileSize: MAX_FILE_SIZE },
    fileFilter: (req, file, cb) => {
        // Check MIME type
        if (!ALLOWED_TYPES[file.mimetype]) {
            return cb(new Error('Invalid file type'), false);
        }
        cb(null, true);
    }
});

app.post('/upload', upload.single('file'), (req, res) => {
    if (!req.file) {
        return res.status(400).json({ error: 'No file uploaded' });
    }

    // Additional validation: check actual file content (magic bytes)
    const fileBuffer = fs.readFileSync(req.file.path);
    const fileType = require('file-type').fromBuffer(fileBuffer);

    if (!fileType || !ALLOWED_TYPES[fileType.mime]) {
        fs.unlinkSync(req.file.path); // Delete invalid file
        return res.status(400).json({ error: 'Invalid file content' });
    }

    res.json({ 
        message: 'File uploaded successfully',
        filename: req.file.filename
    });
});
Enter fullscreen mode Exit fullscreen mode

9. API Rate Limiting

✅ Rate Limiting Middleware:

const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const redis = require('redis');

const redisClient = redis.createClient();

// General API rate limit
const apiLimiter = rateLimit({
    store: new RedisStore({
        client: redisClient,
        prefix: 'rl:api:'
    }),
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100, // 100 requests per windowMs
    message: 'Too many requests, please try again later',
    standardHeaders: true,
    legacyHeaders: false
});

// Stricter limit for authentication endpoints
const authLimiter = rateLimit({
    store: new RedisStore({
        client: redisClient,
        prefix: 'rl:auth:'
    }),
    windowMs: 15 * 60 * 1000,
    max: 5, // Only 5 login attempts per 15 minutes
    skipSuccessfulRequests: true, // Don't count successful logins
    message: 'Too many login attempts, please try again later'
});

app.use('/api/', apiLimiter);
app.use('/api/login', authLimiter);
app.use('/api/register', authLimiter);
Enter fullscreen mode Exit fullscreen mode

10. Secrets Management: Never Hardcode Credentials

❌ Bad: Hardcoded Secrets

// NEVER DO THIS
const API_KEY = 'sk_live_51H7x2y3z4a5b6c7d8e9f0';
const DB_PASSWORD = 'mySecretPassword123';
Enter fullscreen mode Exit fullscreen mode

✅ Good: Environment Variables

// .env file (add to .gitignore!)
/*
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
API_KEY=sk_live_51H7x2y3z4a5b6c7d8e9f0
JWT_SECRET=your-super-secret-jwt-key
SESSION_SECRET=your-session-secret
*/

// Load environment variables
require('dotenv').config();

const config = {
    database: process.env.DATABASE_URL,
    apiKey: process.env.API_KEY,
    jwtSecret: process.env.JWT_SECRET,
    sessionSecret: process.env.SESSION_SECRET
};

// Validate required env vars on startup
const requiredEnvVars = ['DATABASE_URL', 'API_KEY', 'JWT_SECRET'];
for (const envVar of requiredEnvVars) {
    if (!process.env[envVar]) {
        throw new Error(`Missing required environment variable: ${envVar}`);
    }
}
Enter fullscreen mode Exit fullscreen mode

Python Example:

import os
from dotenv import load_dotenv

load_dotenv()

DATABASE_URL = os.getenv('DATABASE_URL')
API_KEY = os.getenv('API_KEY')
SECRET_KEY = os.getenv('SECRET_KEY')

# Validate
if not all([DATABASE_URL, API_KEY, SECRET_KEY]):
    raise ValueError("Missing required environment variables")
Enter fullscreen mode Exit fullscreen mode

11. Security Headers

✅ Essential Security Headers:

const helmet = require('helmet');

app.use(helmet({
    contentSecurityPolicy: {
        directives: {
            defaultSrc: ["'self'"],
            styleSrc: ["'self'", "'unsafe-inline'"],
            scriptSrc: ["'self'"],
            imgSrc: ["'self'", "data:", "https:"],
            connectSrc: ["'self'"],
            fontSrc: ["'self'"],
            objectSrc: ["'none'"],
            mediaSrc: ["'self'"],
            frameSrc: ["'none'"]
        }
    },
    hsts: {
        maxAge: 31536000,
        includeSubDomains: true,
        preload: true
    }
}));

// Or manually
app.use((req, res, next) => {
    // Prevent clickjacking
    res.setHeader('X-Frame-Options', 'DENY');

    // Prevent MIME type sniffing
    res.setHeader('X-Content-Type-Options', 'nosniff');

    // Enable XSS filter
    res.setHeader('X-XSS-Protection', '1; mode=block');

    // Referrer policy
    res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');

    // Permissions policy
    res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');

    next();
});
Enter fullscreen mode Exit fullscreen mode

12. Secure Database Queries: Beyond SQL Injection

✅ Principle of Least Privilege:

-- Create a read-only user for reporting
CREATE USER 'reports'@'localhost' IDENTIFIED BY 'secure_password';
GRANT SELECT ON mydb.* TO 'reports'@'localhost';

-- Create an app user with limited permissions
CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'secure_password';
GRANT SELECT, INSERT, UPDATE ON mydb.users TO 'appuser'@'localhost';
GRANT SELECT, INSERT ON mydb.posts TO 'appuser'@'localhost';

-- Never use root/admin for application connections
Enter fullscreen mode Exit fullscreen mode

Connection String Example:

// Different connection pools for different purposes
const readOnlyPool = mysql.createPool({
    host: process.env.DB_HOST,
    user: 'reports',
    password: process.env.DB_REPORTS_PASSWORD,
    database: process.env.DB_NAME
});

const appPool = mysql.createPool({
    host: process.env.DB_HOST,
    user: 'appuser',
    password: process.env.DB_APP_PASSWORD,
    database: process.env.DB_NAME
});
Enter fullscreen mode Exit fullscreen mode

13. Dependency Scanning in CI/CD

✅ GitHub Actions Security Workflow:

# .github/workflows/security.yml
name: Security Checks

on: [push, pull_request]

jobs:
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Run npm audit
        run: npm audit --audit-level=high

      - name: Run Snyk to check for vulnerabilities
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          args: --severity-threshold=high

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          scan-ref: '.'
          severity: 'CRITICAL,HIGH'
Enter fullscreen mode Exit fullscreen mode

Package.json Scripts:

{
  "scripts": {
    "security-check": "npm audit && npm outdated",
    "security-fix": "npm audit fix",
    "precommit": "npm run security-check"
  }
}
Enter fullscreen mode Exit fullscreen mode

14. Logging Security Events (Without Logging Sensitive Data)

✅ Secure Logging:

const winston = require('winston');

const logger = winston.createLogger({
    level: 'info',
    format: winston.format.json(),
    transports: [
        new winston.transports.File({ filename: 'error.log', level: 'error' }),
        new winston.transports.File({ filename: 'security.log' })
    ]
});

// Log security events
function logSecurityEvent(event, details) {
    logger.info({
        type: 'security',
        event: event,
        timestamp: new Date().toISOString(),
        ip: details.ip,
        userId: details.userId,
        userAgent: details.userAgent,
        // NEVER log passwords, tokens, or sensitive data
    });
}

// Usage in login handler
app.post('/login', async (req, res) => {
    const { username, password } = req.body;
    const user = await verifyUser(username, password);

    if (user) {
        logSecurityEvent('login_success', {
            ip: req.ip,
            userId: user.id,
            userAgent: req.headers['user-agent']
        });
        // ... handle successful login
    } else {
        logSecurityEvent('login_failure', {
            ip: req.ip,
            username: username, // OK to log username attempts
            userAgent: req.headers['user-agent']
        });

        // Check for brute force
        const attempts = await getFailedAttempts(req.ip);
        if (attempts > 5) {
            logSecurityEvent('brute_force_detected', {
                ip: req.ip,
                attempts: attempts
            });
            return res.status(429).json({ 
                error: 'Too many failed attempts' 
            });
        }

        res.status(401).json({ error: 'Invalid credentials' });
    }
});
Enter fullscreen mode Exit fullscreen mode

15. CORS Configuration

❌ Bad: Wide Open CORS

// DANGEROUS - allows any origin
app.use(cors());
Enter fullscreen mode Exit fullscreen mode

✅ Good: Restricted CORS:

const cors = require('cors');

const allowedOrigins = [
    'https://yourapp.com',
    'https://www.yourapp.com'
];

if (process.env.NODE_ENV === 'development') {
    allowedOrigins.push('http://localhost:3000');
}

app.use(cors({
    origin: (origin, callback) => {
        // Allow requests with no origin (mobile apps, Postman)
        if (!origin) return callback(null, true);

        if (allowedOrigins.includes(origin)) {
            callback(null, true);
        } else {
            callback(new Error('Not allowed by CORS'));
        }
    },
    credentials: true, // Allow cookies
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders: ['Content-Type', 'Authorization']
}));
Enter fullscreen mode Exit fullscreen mode

Quick Security Checklist

Before deploying to production, verify:

  • [ ] All passwords are hashed with bcrypt/Argon2
  • [ ] SQL queries use parameterized statements
  • [ ] User input is validated and sanitized
  • [ ] HTTPS is enforced (no HTTP)
  • [ ] Security headers are set (CSP, HSTS, X-Frame-Options)
  • [ ] CORS is properly configured
  • [ ] Rate limiting is enabled
  • [ ] Sessions use httpOnly, secure, sameSite cookies
  • [ ] File uploads are restricted and validated
  • [ ] No secrets in code (use environment variables)
  • [ ] Dependencies are up to date (npm audit)
  • [ ] Error messages don't leak sensitive info
  • [ ] Logging doesn't include passwords/tokens
  • [ ] Database users have minimal permissions
  • [ ] Authentication endpoints have rate limiting

Final Thoughts

Security isn't about being paranoid—it's about being responsible. Every line of code you write is potentially an attack vector. By following these practices and using the code examples above, you'll dramatically reduce your application's attack surface.

Remember: Security is not a feature you add at the end. It's a mindset you adopt from day one. Start with secure defaults, validate everything, trust nothing, and always assume your code will be attacked.

Top comments (0)