Last updated: April 29, 2026
A Hyderabad fintech’s authentication endpoint logged users in via JWT. The team ran cleanly for 18 months until a bug bounty researcher demonstrated that the token validator accepted {"alg": "none"} tokens — anyone could forge an admin session. The cause: a hand-rolled JWT validator written in Node, copy-pasted from a 2018 Stack Overflow answer. Same week, their Python data pipeline was found to deserialise pickle files from S3 — uploads from any AWS user with S3 write access could execute code. Same week, their Java reporting service had a Log4Shell-pattern lookup unpatched. Three languages, three vulnerability classes, one team. This module covers secure coding patterns across the languages Indian shops actually use.
The cross-cutting principles
- Use the framework’s primitives — auth, sessions, CSRF, output encoding. Hand-rolled is where vulnerabilities live
- Validate input at the boundary; trust internal contracts; prefer types over runtime checks
- Output-encode for context — HTML body, attribute, JS, URL, CSS; each context has different escaping rules
- Parameterise queries; never string-build SQL/LDAP/shell
- Fail closed, with helpful errors that don’t leak internals
- Default-deny — explicit allow-list, not deny-list
- Use tested cryptography libraries; never invent
- Keep dependencies current; SCA in CI
Python
Common pitfalls
# BAD — pickle deserialisation of untrusted data = RCE
import pickle
data = pickle.loads(request.body)
# BAD — yaml.load with default loader
import yaml
config = yaml.load(open('config.yaml'))
# BAD — eval / exec on user input
result = eval(user_expression)
# BAD — subprocess with shell=True and user input
subprocess.run(f"convert {filename} out.png", shell=True)
# BAD — SQL string interpolation
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
# GOOD
import json
data = json.loads(request.body)
import yaml
config = yaml.safe_load(open('config.yaml'))
# Use ast.literal_eval for safe expression eval
import ast
result = ast.literal_eval(user_expression)
# Pass args as list, no shell
subprocess.run(['convert', filename, 'out.png'])
# Parameterised SQL
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
Python crypto
# For passwords: argon2-cffi or bcrypt, never sha256
from argon2 import PasswordHasher
ph = PasswordHasher()
hash = ph.hash(password)
ph.verify(hash, candidate)
# For random: secrets module, not random
import secrets
token = secrets.token_urlsafe(32)
Node.js / TypeScript
Common pitfalls
// BAD — JWT 'none' algorithm acceptance
const decoded = jwt.verify(token, key); // some libs default-accept 'none'
// BAD — eval, Function constructor
const result = eval(userExpression);
// BAD — child_process.exec with template string
exec(`tar -czf out.tar.gz ${userFolder}`);
// BAD — SQL with template literals
const rows = await db.query(`SELECT * FROM users WHERE id = ${id}`);
// BAD — open redirect
res.redirect(req.query.next);
// GOOD — explicit algorithms, validated audience and issuer
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'https://idp.example.in/',
audience: 'app.example.in'
});
// GOOD — execFile with array args
execFile('tar', ['-czf', 'out.tar.gz', userFolder]);
// GOOD — parameterised query
const rows = await db.query('SELECT * FROM users WHERE id = $1', [id]);
// GOOD — validate redirect target
const allowed = new Set(['/dashboard', '/settings']);
if (allowed.has(req.query.next)) res.redirect(req.query.next);
else res.redirect('/');
Node crypto
// Random for security-sensitive: crypto.randomBytes, not Math.random
const token = crypto.randomBytes(32).toString('base64url');
// Compare strings in constant time (prevents timing attacks)
const ok = crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
Java
Common pitfalls
// BAD — Log4j JNDI lookup (Log4Shell)
logger.info("User: " + userInput); // pre-2.17 with default config
// BAD — XML parsing with external entities (XXE)
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// Default allows external entities!
// BAD — ObjectInputStream on untrusted bytes
new ObjectInputStream(socket.getInputStream()).readObject();
// BAD — Runtime.exec with single string
Runtime.getRuntime().exec("sh -c " + userCmd);
// BAD — JDBC concatenation
String sql = "SELECT * FROM users WHERE name = '" + name + "'";
stmt.executeQuery(sql);
// GOOD — XXE prevention
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setExpandEntityReferences(false);
// GOOD — PreparedStatement
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM users WHERE name = ?");
ps.setString(1, name);
ResultSet rs = ps.executeQuery();
// GOOD — Log4j 2.17+ with %m{nolookups} or upgrade
// (Log4Shell remains a learnable lesson; check all dependencies)
Go
// Go's stdlib defaults are mostly safe. Common pitfalls:
// BAD — html/template vs text/template confusion
import "text/template" // doesn't auto-escape; XSS
// GOOD
import "html/template" // context-aware escaping
// BAD — exec.Command with string concatenation
cmd := exec.Command("sh", "-c", "ls "+userInput)
// GOOD
cmd := exec.Command("ls", userInput)
// BAD — net/http handlers without timeouts (slowloris)
http.ListenAndServe(":8080", mux)
// GOOD
srv := &http.Server{
Addr: ":8080", Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
srv.ListenAndServe()
Rust
Rust’s memory safety eliminates entire vulnerability classes. Remaining concerns:
unsafeblocks — audit carefully- Logic vulnerabilities — auth, authz, input validation still your job
- Dependencies (the
cargoecosystem has fewer audits than npm/PyPI but still consider supply chain) - Integer overflow in release mode (silently wraps unless using checked operations)
- Deserialisation via
serde— same caution as elsewhere; don’t deserialise untrusted into types with allocation hooks
PHP
// Modern PHP (8.x) with frameworks (Laravel, Symfony) is much safer than legacy.
// Common pitfalls in legacy code:
// BAD — file inclusion with user input
include $_GET['page'] . '.php'; // LFI / RFI
// BAD — extract($_REQUEST) — variable pollution
// BAD — unserialize on user input
$data = unserialize($_COOKIE['prefs']);
// BAD — direct query
$result = mysql_query("SELECT * FROM users WHERE id = " . $_GET['id']);
// GOOD — PDO prepared statements
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$id]);
// GOOD — htmlspecialchars on output
echo htmlspecialchars($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
// GOOD — password_hash / password_verify
$hash = password_hash($password, PASSWORD_ARGON2ID);
if (password_verify($candidate, $hash)) { /* ok */ }
The Hyderabad fintech rebuild
- Hand-rolled JWT replaced with
node-josewith explicit alg pinning + audience + issuer validation - Pickle replaced with JSON for cross-service data; for internal Python pipelines using pickle, signature validation added (
cryptographyHMAC) - Log4j upgraded to 2.20+; SCA in CI to block any vulnerable component version going forward
- Semgrep rules added for each fixed pattern; same vulnerability cannot land twice
- Quarterly secure-coding workshop using language-specific OWASP cheat sheets
Indian compliance mapping
- RBI Cyber Framework — secure coding standards for in-house development
- SEBI CSCRF — secure development lifecycle for Q-RE / MII
- DPDP §10 — secure-by-design for SDFs
- UIDAI — secure coding for AUA / KUA implementations
- ISO 27001:2022 A.8.28 — secure coding
- OWASP ASVS — application security verification standard, increasingly cited in Indian RFPs
Try this in your environment
- Run language-appropriate SAST (Bandit for Python, ESLint security plugins + Semgrep for JS, SpotBugs for Java, gosec for Go) on your code. Fix the top 10 highest-severity findings.
- Search your codebase for:
eval(,pickle.loads,yaml.load,shell=True,jwt.verify(.*[^,]*)with no algorithm. Each match is a candidate for review. - Run dependency scanning (Snyk, Dependabot, npm audit, pip-audit, mvn dependency-check). Triage critical/high.
- Pick the most-edited file in your repo. Review for the patterns above.
- Establish “every fixed vulnerability becomes a Semgrep rule” as a team standard.
Secure coding is language-specific in surface but universal in principles. The Hyderabad fintech had three problems in three languages because the engineering culture didn’t have the discipline. Build the discipline; the languages take care of themselves.
Module Quiz · 6 questions
Pass with 80%+ to mark this module complete. Unlimited retries. Each question shows an explanation.
Get a VAPT scoping call
Senior practitioner-led VAPT — not a checklist run by juniors. CVSS-scored findings, free retest, attestation letter. India's SMBs and SaaS teams.