Developers, It's Time to Talk Security
Security habits I actually use in 2025. Validate input, hash passwords, patch deps, HTTPS, headers, and baking checks into the SDLC.
Building apps is fun. Shipping holes isn't. Every form and API route is a door. In 2025 the threats are boring and automated: credential stuffing, SQLi, XSS, leaked keys in repos.
Security isn't a handoff to IT the night before launch. It belongs in design, code review, CI, and ops.
Input validation first
Treat every input as hostile until proven otherwise. Client-side checks are UX. Server-side checks are the real gate.
SQL injection dies with parameterized queries or an ORM that doesn't concatenate strings.
Express example with express-validator:
const { body, validationResult } = require("express-validator");
app.post(
"/register",
[
body("username")
.trim()
.escape()
.isLength({ min: 3 })
.withMessage("Username must be at least 3 characters long"),
body("email").isEmail().normalizeEmail().withMessage("Invalid email address"),
body("password")
.isLength({ min: 8 })
.withMessage("Password must be at least 8 characters long"),
],
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
res.status(200).send("User registered successfully!");
},
);
Auth and access control
OWASP still ranks broken auth high. MFA where you can. Ban weak passwords. Hash with bcrypt or Argon2, never plaintext.
Sessions: long random IDs, rotate on login, HttpOnly + Secure cookies. Default deny on authorization. Grant the minimum role that works.
const bcrypt = require("bcrypt");
const saltRounds = 10;
async function hashPassword(password) {
return bcrypt.hash(password, saltRounds);
}
async function comparePassword(password, hashedPassword) {
return bcrypt.compare(password, hashedPassword);
}
Dependencies
Your app is mostly other people's code. Run Dependabot, Snyk, or OWASP Dependency-Check. Update boring patch releases before they're emergency fire drills.
Before adding a package, check maintenance and issue history.
HTTPS and headers
Encrypt everything in transit. Let's Encrypt is free. Redirect HTTP to HTTPS. Add HSTS so browsers remember.
server {
listen 80;
server_name pantelis.theodosiou.me;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name pantelis.theodosiou.me;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
CSP cuts XSS damage. X-Frame-Options helps against clickjacking. They're cheap wins.
Privacy
GDPR/CCPA aren't only legal problems. Collect less. Explain what you store. Make export and delete possible. Encrypt sensitive fields at rest and in transit.
Logging and monitoring
Log auth events and permission changes. Don't log passwords, tokens, or full card numbers. Centralize logs if you can. Alert on weird patterns (geo jumps, brute force).
Shift left
Threat model before the sprint ends. Security training isn't once a year checkbox theater. SAST in CI, DAST on staging, pen tests before big launches.
Culture matters as much as tools. One dev pasting keys in Slack undoes a week of hardening.
Build it right. Build it boring-safe. Your users don't get a staging environment for their data.