# Stop Shipping Garbage: The 2026 Playbook for Secure Coding
Look, I'm tired. I review PRs all day, and I keep seeing the same basic security footguns being shipped to prod by engineers who should know better. We have AI completing our code, yet we're still falling for 1999-era vulnerabilities. The so-called secure software development lifecycle (SDLC security) in most orgs is a joke. It’s a Jira ticket someone clicks ‘Done’ on, while raw string concatenation sneaks into the database layer.
Bottom line: secure coding practices aren't optional anymore. You can't just slap a WAF in front of your janky Node.js backend and call it a day. This is the playbook. Follow these secure coding guidelines or get breached. I don't care about your sprint velocity if you're actively compromising user data.
1. Input Validation Best Practices: Zod or GTFO
I see a lot of if (req.body.userId == null) checks. Stop. Just stop. That’s not input validation. That’s a prayer. You need strict schema validation at the absolute edges of your application. If it doesn't match the schema, nuke it. No exceptions.
I'm tired of seeing this in PRs. This is where OWASP secure coding rules meet reality. We use Zod. Why? Because it enforces strict typing and strips out garbage under the hood.
import { z } from 'zod';
import express from 'express';
const app = express();
app.use(express.json());
// Strict schema. No extra keys allowed.
const UserProfileSchema = z.object({
username: z.string().min(3).max(30).regex(/^[a-zA-Z0-9_]+$/),
email: z.string().email(),
age: z.number().int().min(18).max(120),
}).strict();
app.post('/api/profile', (req, res) => {
const result = UserProfileSchema.safeParse(req.body);
if (!result.success) {
// Log this internally, but give a generic 400 to the client
console.error('Validation failed:', result.error.format());
return res.status(400).json({ error: 'Invalid input data.' });
}
// result.data is now 100% typed and scrubbed
const validData = result.data;
// proceed to business logic
});If you aren't doing this, you are begging for NoSQL injection or prototype pollution. Strict schemas mean attackers hit a brick wall before they even reach your business logic. Input validation best practices are your first line of defense.
2. Parameterized Queries: The SQLi Epidemic Is Still Here
I can't believe I have to write this in 2026. If you use string interpolation for SQL queries, you should have your commit access revoked. I don't care if it's an internal admin tool. Internal tools are how lateral movement happens.
Look at this garbage:
// DO NOT DO THIS. EVER.
const query = `SELECT * FROM users WHERE email = '${userEmail}'`;
db.query(query);Look, I've seen this a hundred times. Here is how you actually do it with parameterized queries. The database driver handles the sanitization. You don't try to escape it yourself.
// GOOD: Using parameterized queries with pg (PostgreSQL)
const query = 'SELECT * FROM users WHERE email = $1';
const values = [userEmail];
try {
const res = await db.query(query, values);
return res.rows;
} catch (err) {
// Generic error to the user
throw new Error('Database operation failed');
}And if you use an ORM like Prisma or Drizzle, don't use their raw query bypasses unless you absolutely have to, and even then, use the tagged template literals they provide. SDLC security mandates parameterized inputs everywhere.
3. Output Encoding: Killing XSS for Good
React and modern frameworks encode by default, but developers love to find ways to shoot themselves in the foot with dangerouslySetInnerHTML. If you render user-supplied input without output encoding, you get XSS.
When you do have to render raw HTML (like a markdown parser), use DOMPurify. And do it on the server if possible.
import DOMPurify from 'isomorphic-dompurify';
const rawHtml = getUserInput();
// Sanitize before it ever hits the DOM or gets saved to the DB
const cleanHtml = DOMPurify.sanitize(rawHtml, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
});4. CSP Headers: Defense In Depth
Honestly, it's a nightmare. Content Security Policy (CSP) is a pain in the ass to configure. I get it. Your analytics script breaks. Your font stops loading. But it is the ultimate safety net for XSS. If you mess up output encoding, CSP stops the payload from executing.
Use Helmet in Express. Set a strict CSP.
import helmet from 'helmet';
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://trusted.cdn.com"],
objectSrc: ["'none'"],
upgradeInsecureRequests: [],
},
})
);Do not use 'unsafe-inline'. If you have inline scripts, refactor them. Yes, it takes work. Do it anyway.
5. CORS Configuration: Stop Using Wildcards
I see Access-Control-Allow-Origin: * in PRs all the time. This is a massive red flag. If your API is authenticated via cookies, an open CORS policy is a disaster waiting to happen.
Honestly, it's a nightmare. Be explicit.
import cors from 'cors';
const whitelist = ['https://app.bryxe.com', 'https://admin.bryxe.com'];
const corsOptions = {
origin: function (origin, callback) {
if (!origin || whitelist.indexOf(origin) !== -1) {
callback(null, true)
} else {
callback(new Error('Not allowed by CORS'))
}
},
credentials: true // Only if you actually need cookies
};
app.use(cors(corsOptions));6. Secure Session Management
JWTs are not a magic bullet. If you store a JWT in localStorage, any XSS vulnerability immediately compromises the session.
Use HttpOnly, Secure, SameSite=Strict cookies.
app.post('/login', (req, res) => {
// ... validate user ...
const sessionId = generateSecureSessionId();
res.cookie('sessionId', sessionId, {
httpOnly: true, // No JS access
secure: process.env.NODE_ENV === 'production', // HTTPS only
sameSite: 'strict', // CSRF protection
maxAge: 3600000 // 1 hour
});
res.json({ message: 'Logged in' });
});7. Error Handling Without Info Leaks
When things break, your stack trace should never hit the client. Never. It tells attackers exactly what versions of what libraries you run, absolute paths on your server, and sometimes even database structure.
I'm tired of seeing this in PRs. Catch errors globally. Log the details to your SIEM. Return a generic 500 to the user.
app.use((err, req, res, next) => {
// Log the full stack trace internally
logger.error('Unhandled Exception:', err);
// Return generic response
res.status(500).json({
error: 'An unexpected error occurred. Please try again later.'
});
});8. Cryptographic Best Practices
Stop inventing your own crypto. Stop using MD5 or SHA-1 for passwords. Use Argon2id or bcrypt. In 2026, Argon2id is the standard.
import * as argon2 from 'argon2';
async function hashPassword(password: string) {
try {
return await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 2 ** 16,
timeCost: 3,
parallelism: 1,
});
} catch (err) {
throw new Error('Hashing failed');
}
}When dealing with encryption at rest for PII, use AES-256-GCM. It provides authenticated encryption. Don't touch ECB mode. Don't touch CBC without HMAC. Just use GCM and be done with it.
Wrapping Up
Look, secure coding practices aren't magic. It’s discipline. It’s refusing to merge sloppy PRs. It’s building SDLC security into your pipelines with SAST/DAST tools so this stuff gets caught automatically.
I'm tired of seeing this in PRs. Start implementing these patterns today. Clean up your Zod schemas. Fix your CORS. Nuke your wildcards. Your on-call rotation will thank you when you don't get paged at 3 AM for a data breach.
![Secure Coding Practices: The Senior Engineer's Playbook [2026 Audit]](/_next/image?url=%2Fblog%2Ftest-mockup.jpg&w=3840&q=75&dpl=dpl_9wjFGoCwx1HVyawLu6p7BBQYdgG6)