Secure Coding Across Languages

Manish Garg
Manish Garg Associate of (ISC)² · RingSafe
Apr 26, 2026
6 min read
Read as

Last updated: April 29, 2026

Language-specific secure-coding patterns — Python, Node/TS, Java, Go, Rust, PHP. Common pitfalls, safe alternatives, crypto patterns, dependency scanning.

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:

  • unsafe blocks — audit carefully
  • Logic vulnerabilities — auth, authz, input validation still your job
  • Dependencies (the cargo ecosystem 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-jose with explicit alg pinning + audience + issuer validation
  • Pickle replaced with JSON for cross-service data; for internal Python pipelines using pickle, signature validation added (cryptography HMAC)
  • 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

  1. 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.
  2. Search your codebase for: eval(, pickle.loads, yaml.load, shell=True, jwt.verify(.*[^,]*) with no algorithm. Each match is a candidate for review.
  3. Run dependency scanning (Snyk, Dependabot, npm audit, pip-audit, mvn dependency-check). Triage critical/high.
  4. Pick the most-edited file in your repo. Review for the patterns above.
  5. 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.

🧠
Check your understanding

Module Quiz · 6 questions

Pass with 80%+ to mark this module complete. Unlimited retries. Each question shows an explanation.

Need a real pentest?

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.

Book VAPT scoping call Replies in 4 working hrs · India-only · Senior consultants