Secure Coding Practices — A Beginner's Guide to Writing Secure Code
Secure coding is the practice of writing software that is resistant to security vulnerabilities by applying defensive programming techniques — validating input, encoding output, managing authentication securely, and avoiding common weaknesses from the start.
What You’ll Learn
By the end of this tutorial, you’ll understand the OWASP Application Security Verification Standard (ASVS), implement secure input validation and output encoding, follow authentication and session management best practices, and use SAST tools to catch vulnerabilities during development.
Why Secure Coding Matters
Over 70% of security vulnerabilities originate in application code, not infrastructure. Writing secure code from the start costs 10x less than fixing vulnerabilities after deployment. At DodaTech, Durga Antivirus Pro undergoes rigorous secure code reviews before every release.
Secure Coding Learning Path
flowchart LR
A[Web Security] --> B[DevSecOps]
B --> C[Secure Coding]
C --> D{You Are Here}
style D fill:#f90,color:#fff
What Is Secure Coding? (The “Why” First)
Think of secure coding like defensive driving. You don’t drive assuming everyone follows the rules — you anticipate that someone might run a red light, merge without signaling, or brake suddenly. Secure coding is the same: you don’t assume input is safe, users are authenticated, or third-party code is secure.
OWASP ASVS Overview
The OWASP Application Security Verification Standard defines four levels of security verification:
| Level | Description | When to Use |
|---|---|---|
| L1 | Automated verification | All applications (minimum) |
| L2 | Manual + automated | Applications handling sensitive data |
| L3 | In-depth verification | Critical applications (finance, healthcare, defense) |
Core Secure Coding Principles
1. Input Validation — Never Trust User Input
Validate everything that comes from outside your application:
# input_validation.py — Secure input validation examples
import re
from typing import Optional
class InputValidator:
"""Demonstrates secure input validation patterns."""
@staticmethod
def validate_email(email: str) -> Optional[str]:
"""Validate and normalize an email address."""
if not email or len(email) > 254:
return None
email = email.strip().lower()
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if re.match(pattern, email):
return email
return None
@staticmethod
def validate_username(username: str) -> Optional[str]:
"""Validate username: alphanumeric, 3-30 chars."""
if not username:
return None
username = username.strip()
if re.match(r'^[a-zA-Z0-9_]{3,30}$', username):
return username
return None
@staticmethod
def sanitize_filename(filename: str) -> str:
"""Remove dangerous characters from filenames."""
# Remove path traversal attempts
safe = filename.replace('..', '')
# Keep only safe characters
safe = re.sub(r'[^\w\-\.]', '_', safe)
# Limit length
return safe[:255]
@staticmethod
def validate_integer(value: str, min_val: int, max_val: int) -> Optional[int]:
"""Safely parse and validate an integer."""
try:
num = int(value)
if min_val <= num <= max_val:
return num
return None
except (ValueError, TypeError):
return None
# Example usage
validator = InputValidator()
print(validator.validate_email("user@example.com")) # user@example.com
print(validator.validate_email("not-an-email")) # None
print(validator.sanitize_filename("../../etc/passwd")) # etc_passwdValidation rules:
- Whitelist — define what’s allowed (safer than blacklist)
- Length limits — prevent buffer overflow, DoS
- Type checking — integers are integers, not strings
- Format validation — regex patterns for emails, phone numbers, etc.
2. Output Encoding — Don’t Execute Untrusted Content
Always encode data before rendering it in a different context:
# output_encoding.py — Output encoding in different contexts
class OutputEncoder:
"""Demonstrates secure output encoding for different contexts."""
@staticmethod
def html_encode(text: str) -> str:
"""Encode text for HTML context (prevents XSS)."""
replacements = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
}
for char, encoded in replacements.items():
text = text.replace(char, encoded)
return text
@staticmethod
def html_attribute_encode(text: str) -> str:
"""Encode text for HTML attribute context."""
# More aggressive encoding needed in attributes
text = text.replace('"', '"')
text = text.replace("'", ''')
text = text.replace('`', '`')
text = text.replace(' ', ' ')
return text
@staticmethod
def javascript_encode(text: str) -> str:
"""Encode text for JavaScript string context."""
replacements = {
'\\': '\\\\',
"'": "\\'",
'"': '\\"',
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
'<': '\\x3C',
'>': '\\x3E',
}
for char, encoded in replacements.items():
text = text.replace(char, encoded)
return text
@staticmethod
def url_encode(text: str) -> str:
"""Encode text for URL context."""
import urllib.parse
return urllib.parse.quote(text, safe='')
# Example
encoder = OutputEncoder()
user_input = "<script>alert('xss')</script>"
print(f"HTML encoded: {encoder.html_encode(user_input)}")
# <script>alert('xss')</script>Context matters:
- HTML body →
<not< - HTML attribute → Encode quotes and spaces
- JavaScript string → Escape
',",\,\n - URL → Percent-encode special characters
- SQL → Use parameterized queries (never escape manually)
3. Authentication — Verify Identity Securely
# authentication_secure.py — Authentication best practices
import hashlib
import secrets
import os
class SecureAuth:
"""Demonstrates secure authentication practices."""
@staticmethod
def hash_password(password: str) -> tuple[str, str]:
"""Hash a password using bcrypt (not MD5 or SHA-1!)."""
# In production use bcrypt, Argon2, or scrypt
# This example uses PBKDF2 which is acceptable
salt = os.urandom(32)
key = hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt,
100000, # Iterations — should be updated as hardware improves
dklen=32
)
return salt.hex(), key.hex()
@staticmethod
def verify_password(password: str, salt_hex: str, key_hex: str) -> bool:
"""Verify a password against stored hash."""
salt = bytes.fromhex(salt_hex)
stored_key = bytes.fromhex(key_hex)
key = hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt,
100000,
dklen=32
)
return secrets.compare_digest(key, stored_key)
@staticmethod
def generate_session_token() -> str:
"""Generate a cryptographically secure session token."""
return secrets.token_hex(32) # 256 bits of entropy
@staticmethod
def verify_mfa_code(secret: str, code: str) -> bool:
"""Verify a TOTP-based MFA code."""
# In production use pyotp library
# TOTP: Time-based One-Time Password
import hmac
import time
interval = int(time.time()) // 30 # 30-second window
key = secret.encode()
msg = interval.to_bytes(8, 'big')
expected = hmac.new(key, msg, 'sha1').digest()[:4]
# Convert to 6-digit code
expected_code = str(int.from_bytes(expected, 'big') % 10**6).zfill(6)
return hmac.compare_digest(code, expected_code)
# Example
salt, key = SecureAuth.hash_password("MyS3cur3P@ss!")
print(f"Salt: {salt[:16]}...")
print(f"Key: {key[:16]}...")
print(f"Verify: {SecureAuth.verify_password('MyS3cur3P@ss!', salt, key)}") # True
print(f"Wrong: {SecureAuth.verify_password('wrong', salt, key)}") # FalseAuthentication checklist:
- Password hashing: Use bcrypt, Argon2, or scrypt — never MD5, SHA-1, or unsalted hashes
- MFA: Always offer multi-factor authentication
- Account lockout: Lock after 5-10 failed attempts (with gradual delays)
- Rate limiting: Limit login attempts per IP and per user
- Session tokens: Cryptographically random, 256+ bits
- Secure storage: Hash passwords, never store plaintext
4. Session Management — Protect Active Sessions
# session_management.py — Secure session management
class SecureSession:
"""Demonstrates secure session management practices."""
def __init__(self):
self.sessions = {} # In production, use Redis or similar
def create_session(self, user_id: str, mfa_verified: bool = False) -> str:
"""Create a new session with security context."""
import secrets
import time
session_id = secrets.token_hex(32)
self.sessions[session_id] = {
"user_id": user_id,
"created_at": time.time(),
"last_activity": time.time(),
"ip_address": None, # Set on first request
"user_agent": None, # Set on first request
"mfa_verified": mfa_verified,
"impersonating": False
}
return session_id
def validate_session(self, session_id: str,
ip_address: str,
user_agent: str,
timeout_minutes: int = 30) -> bool:
"""Validate a session with security checks."""
import time
session = self.sessions.get(session_id)
if not session:
return False
# Check timeout
elapsed = time.time() - session["last_activity"]
if elapsed > timeout_minutes * 60:
del self.sessions[session_id]
return False
# Check IP address change (optional — can be problematic for mobile)
# if session["ip_address"] and session["ip_address"] != ip_address:
# return False # Possible session hijacking
# Update last activity
session["last_activity"] = time.time()
if not session["ip_address"]:
session["ip_address"] = ip_address
if not session["user_agent"]:
session["user_agent"] = user_agent
return True
def invalidate_session(self, session_id: str):
"""Log out a session."""
self.sessions.pop(session_id, None)
def rotate_session(self, session_id: str) -> str:
"""Session rotation after privilege escalation."""
session = self.sessions.get(session_id)
if not session:
return None
self.invalidate_session(session_id)
return self.create_session(
session["user_id"],
session.get("mfa_verified", False)
)Session security rules:
- Regenerate session ID after login and privilege changes
- Set
HttpOnlyandSecureflags on cookies - Use
SameSite=StrictorLax - Set session timeout (15-30 minutes of inactivity)
- Never expose session IDs in URLs
5. Error Handling — Don’t Leak Information
# error_handling.py — Secure error handling
class SecureErrorHandler:
"""Demonstrates secure error handling — never expose internals."""
@staticmethod
def safe_error_response(error: Exception, debug_mode: bool = False) -> dict:
"""Return a safe error response without leaking internals."""
# Map to user-safe message
error_map = {
ValueError: "Invalid input provided.",
PermissionError: "You don't have permission to perform this action.",
FileNotFoundError: "The requested resource was not found.",
ConnectionError: "A temporary error occurred. Please try again.",
}
# Get safe message
error_type = type(error)
user_message = error_map.get(error_type, "An unexpected error occurred.")
# Build response
response = {
"error": True,
"message": user_message,
"status_code": 500
}
# Only in debug mode — log details internally
if debug_mode:
print(f"DEBUG — {error_type.__name__}: {str(error)}")
# In production, use proper logging:
# logging.exception(f"Error processing request: {error}")
return response
@staticmethod
def log_error(error: Exception, context: dict = None):
"""Securely log errors — never log sensitive data."""
import logging
# Sanitize context — remove sensitive fields
sensitive_keys = {"password", "secret", "token", "authorization",
"credit_card", "ssn", "api_key"}
safe_context = {}
if context:
for k, v in context.items():
if k.lower() in sensitive_keys:
safe_context[k] = "[REDACTED]"
else:
safe_context[k] = v
logging.error(f"Error: {type(error).__name__}: {error}", extra=safe_context)
# Example
try:
raise ValueError("Database connection failed: server=prod-db-01:3306")
except ValueError as e:
response = SecureErrorHandler.safe_error_response(e, debug_mode=True)
print(f"User sees: {response['message']}")
# User sees: "Invalid input provided."
# Not: "Database connection failed: server=prod-db-01:3306"6. File Upload Security
# file_upload.py — Secure file upload handling
class SecureFileUpload:
"""Demonstrates secure file upload practices."""
ALLOWED_TYPES = {
'image/jpeg': '.jpg',
'image/png': '.png',
'image/gif': '.gif',
'application/pdf': '.pdf',
'text/plain': '.txt',
}
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB
@classmethod
def validate_upload(cls, file_data: bytes,
content_type: str,
filename: str) -> tuple[bool, str]:
"""Validate a file upload for security."""
# Check file size
if len(file_data) > cls.MAX_FILE_SIZE:
return False, "File exceeds maximum size (5MB)"
# Check content type
if content_type not in cls.ALLOWED_TYPES:
return False, f"File type '{content_type}' is not allowed"
# Verify extension matches content type
expected_ext = cls.ALLOWED_TYPES[content_type]
if not filename.lower().endswith(expected_ext):
return False, "File extension does not match content type"
# Verify magic bytes (not just trusting the MIME type)
if content_type == 'image/jpeg':
if not file_data.startswith(b'\xff\xd8\xff'):
return False, "File is not a valid JPEG"
elif content_type == 'image/png':
if not file_data.startswith(b'\x89PNG\r\n\x1a\n'):
return False, "File is not a valid PNG"
return True, "File is valid"
@classmethod
def secure_filename(cls, filename: str) -> str:
"""Generate a safe filename for storage."""
import uuid
import re
# Keep only safe characters
safe = re.sub(r'[^\w\-\.]', '_', filename)
# Prepend UUID to prevent path traversal and overwrites
ext = safe.split('.')[-1] if '.' in safe else ''
return f"{uuid.uuid4().hex}.{ext}"Common Secure Coding Mistakes
1. Trusting User Input
The most common vulnerability. Validate, sanitize, and encode everything from users.
2. Rolling Your Own Cryptography
Never write your own encryption or hash functions. Use well-vetted libraries. Homemade crypto is almost always broken.
3. Logging Sensitive Information
Passwords, credit cards, and session tokens should never appear in logs. Implement log sanitization.
4. Not Handling All Error Paths
A try/except that catches all exceptions and does nothing silently hides security-relevant errors. Always log and handle appropriately.
5. Using Insecure Randomness
random module is for non-security purposes. Use secrets (Python) or crypto.randomBytes (Node.js) for security.
6. Hardcoding Secrets
API keys, passwords, and tokens in source code will eventually leak. Use environment variables or a secrets manager.
7. Ignoring Third-Party Dependencies
Your code may be secure, but a vulnerable npm package or pip library can compromise everything. Run SCA tools regularly.
Practice Questions
1. What is the difference between input validation and output encoding?
Input validation rejects bad data before processing. Output encoding transforms data for safe rendering in different contexts (HTML, JavaScript, SQL). Both are needed — they solve different problems.
2. Why should you never use MD5 or SHA-1 for password hashing?
They’re designed for speed, making them easy to brute force. Use bcrypt, Argon2, or scrypt which are intentionally slow and include salts.
3. What is session fixation and how do you prevent it?
An attacker sets a user’s session ID before they log in. Prevent by regenerating the session ID after login (session rotation).
4. Why should error messages not reveal internal details?
Attackers use error messages to learn about your infrastructure: server names, database types, file paths, and code structure. Always return generic user-safe messages.
5. Challenge: Write a secure function that validates and sanitizes a URL provided by user input.
Parse with urllib.parse, whitelist http/https schemes, validate format, encode special characters, and verify the domain isn’t an IP address (if policy requires domains only).
Mini Project: Secure Code Reviewer
# secure_code_reviewer.py
# Automated security review for common code issues
import ast
import re
class SecureCodeReviewer:
"""Scan Python source code for security issues."""
CHECKS = {
"hardcoded_password": {
"pattern": r'(password|passwd|secret|api_key)\s*=\s*["\'][^"\']+["\']',
"severity": "HIGH",
"message": "Possible hardcoded credential"
},
"eval_usage": {
"pattern": r'\beval\s*\(',
"severity": "CRITICAL",
"message": "Use of eval() — code injection risk"
},
"exec_usage": {
"pattern": r'\bexec\s*\(',
"severity": "CRITICAL",
"message": "Use of exec() — code injection risk"
},
"insecure_hash": {
"pattern": r'hashlib\.(md5|sha1)\b',
"severity": "MEDIUM",
"message": "Use of weak hashing algorithm"
},
"sql_injection": {
"pattern": r'execute\(.*["\'].*\{.*["\']\)',
"severity": "CRITICAL",
"message": "Possible SQL injection (string interpolation in query)"
},
"pickle_usage": {
"pattern": r'import pickle|pickle\.(load|loads)',
"severity": "HIGH",
"message": "Pickle deserialization can execute arbitrary code"
},
"assert_usage": {
"pattern": r'\bassert\b',
"severity": "LOW",
"message": "Assertions disabled with -O flag — use proper validation"
},
}
def review_file(self, filepath: str) -> list[dict]:
"""Review a Python file for security issues."""
findings = []
try:
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
lines = content.split('\n')
except (FileNotFoundError, IOError) as e:
return [{"error": f"Cannot read file: {e}"}]
for check_name, config in self.CHECKS.items():
for i, line in enumerate(lines, 1):
if re.search(config["pattern"], line):
findings.append({
"file": filepath,
"line": i,
"severity": config["severity"],
"check": check_name,
"message": config["message"],
"code": line.strip()
})
return findings
# Example review
reviewer = SecureCodeReviewer()
sample_code = """
import hashlib
password = "SuperSecret123!"
eval(user_input)
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
"""
with open("/tmp/vulnerable_code.py", "w") as f:
f.write(sample_code)
results = reviewer.review_file("/tmp/vulnerable_code.py")
for r in results:
print(f"[{r['severity']}] Line {r['line']}: {r['message']}")
print(f" Code: {r['code']}")FAQ
Try It Yourself
- Take a piece of code you’ve written recently (or use the vulnerable sample above)
- Run it through a SAST tool like Bandit:
pip install bandit && bandit -r . - Fix each finding
- Verify with
banditagain
This is the same workflow DodaTech engineers follow for Doda Browser and Durga Antivirus Pro development — write, scan, fix, verify.
What’s Next
What’s Next
Congratulations on completing this Secure Coding tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro