Secure Coding
Purpose
Write code where the secure path is the default path. Most vulnerabilities are not clever — they are a string concatenated into a query, an authorization check that was never written, or a password hashed with the wrong algorithm.
When to Use
- Writing code that handles user input, authentication, or sensitive data.
- Implementing authorization.
- Handling passwords, tokens, or encryption.
- Reviewing code for vulnerabilities.
Capabilities
- Injection prevention: SQL, command, template, LDAP, XSS.
- Authentication: password storage, session management, MFA, token handling.
- Authorization: enforcing it at the right layer, and the object-level checks people forget.
- Cryptography: choosing the right primitive and not implementing it yourself.
- Secure defaults: headers, cookies, TLS.
Inputs
- The code, and where untrusted data enters it.
- The authentication and authorization model.
- Compliance requirements, if any.
Outputs
- Parameterized queries and encoded output, everywhere.
- Authorization enforced on every object access, not just at the route.
- Secrets and credentials handled with the correct primitives.
Workflow
- Identify the trust boundaries — Every place data enters from outside: HTTP, files, queues, environment, third-party APIs. Everything crossing one is untrusted, including data from your own other services.
- Parameterize, never concatenate — Every query, command, and template. String interpolation into SQL is the oldest vulnerability there is, and it is still the most common.
- Authorize on the object, not the route — A route guard checks that you are logged in. It does not check that this order belongs to you. Broken object-level authorization is the most prevalent API vulnerability in practice.
- Use the right crypto primitive — Argon2id or bcrypt for passwords. AES-GCM or libsodium for encryption. HMAC for signing. Never design your own scheme, and never use MD5, SHA-1, or plain SHA-256 for passwords.
- Fail closed — On error, deny. An authorization check that throws and is caught by a generic handler returning 200 has granted access.
- Set the defaults — Secure cookies, CSP, HSTS, and TLS configuration. These are one-time changes that eliminate whole vulnerability classes.
Best Practices
- An ORM does not make you safe from SQL injection if you use its raw-query escape hatch with an f-string.
- Never log a password, token, session ID, or full card number — including in an exception's message or a request dump.
- Comparing secrets with
== leaks their length and content through timing. Use a constant-time comparison.
- A JWT with
alg: none accepted by your verifier is a full authentication bypass. Pin the algorithm; never trust the header's claim about it.
- Validate on an allowlist, not a denylist. You cannot enumerate every malicious input, but you can enumerate every valid one.
- Rate-limit authentication endpoints. Without it, a leaked password list is a working login.
Examples
The vulnerability that route-level auth does not catch:
# The route requires a login. It does not check that the order is the user's.
@router.get("/orders/{order_id}")
@requires_auth # authenticated, but not authorized
async def get_order(order_id: str, user: User = Depends(current_user)):
return await db.orders.get(order_id) # any user can read any order
# Correct: authorization is a property of the object, not the route.
@router.get("/orders/{order_id}")
async def get_order(order_id: str, user: User = Depends(current_user)):
order = await db.orders.get(order_id)
if order is None or order.customer_id != user.id:
# Same response for "does not exist" and "not yours": do not leak existence.
raise HTTPException(404, "Order not found")
return order
Passwords and token comparison:
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
import hmac
ph = PasswordHasher() # Argon2id, correct parameters by default
def hash_password(plain: str) -> str:
return ph.hash(plain) # salt is generated and embedded
def verify_password(plain: str, stored: str) -> bool:
try:
ph.verify(stored, plain)
return True
except VerifyMismatchError:
return False
def verify_webhook(signature: str, expected: str) -> bool:
# `==` on secrets leaks information through timing. This does not.
return hmac.compare_digest(signature, expected)
Parameterized, always:
# Injectable. The ORM does not save you here.
await db.execute(f"SELECT * FROM orders WHERE status = '{status}'")
# Safe.
await db.execute("SELECT * FROM orders WHERE status = :status", {"status": status})
Notes
- Broken object-level authorization (BOLA/IDOR) is consistently the top API vulnerability. Every endpoint that takes an ID must check that the caller may access that object. A route guard is not enough.
- Returning 404 rather than 403 for objects the user may not access prevents enumeration — a 403 confirms the object exists.
- Argon2id is the current recommendation for password hashing. bcrypt remains acceptable. PBKDF2 is acceptable where required by compliance. A bare hash — even SHA-256 — is not password hashing, it is a lookup table waiting to be built.
1---2name: secure-coding3description: Use when writing code that handles untrusted input, authentication, or sensitive data. Covers injection prevention, authentication and session handling, authorization, cryptography, and the defaults that make code safe by construction.4---56# Secure Coding78## Purpose910Write code where the secure path is the default path. Most vulnerabilities are not clever — they are a string concatenated into a query, an authorization check that was never written, or a password hashed with the wrong algorithm.1112## When to Use1314- Writing code that handles user input, authentication, or sensitive data.15- Implementing authorization.16- Handling passwords, tokens, or encryption.17- Reviewing code for vulnerabilities.1819## Capabilities2021- Injection prevention: SQL, command, template, LDAP, XSS.22- Authentication: password storage, session management, MFA, token handling.23- Authorization: enforcing it at the right layer, and the object-level checks people forget.24- Cryptography: choosing the right primitive and not implementing it yourself.25- Secure defaults: headers, cookies, TLS.2627## Inputs2829- The code, and where untrusted data enters it.30- The authentication and authorization model.31- Compliance requirements, if any.3233## Outputs3435- Parameterized queries and encoded output, everywhere.36- Authorization enforced on every object access, not just at the route.37- Secrets and credentials handled with the correct primitives.3839## Workflow40411. **Identify the trust boundaries** — Every place data enters from outside: HTTP, files, queues, environment, third-party APIs. Everything crossing one is untrusted, including data from your own other services.422. **Parameterize, never concatenate** — Every query, command, and template. String interpolation into SQL is the oldest vulnerability there is, and it is still the most common.433. **Authorize on the object, not the route** — A route guard checks that you are logged in. It does not check that *this* order belongs to *you*. Broken object-level authorization is the most prevalent API vulnerability in practice.444. **Use the right crypto primitive** — Argon2id or bcrypt for passwords. AES-GCM or libsodium for encryption. HMAC for signing. Never design your own scheme, and never use MD5, SHA-1, or plain SHA-256 for passwords.455. **Fail closed** — On error, deny. An authorization check that throws and is caught by a generic handler returning 200 has granted access.466. **Set the defaults** — Secure cookies, CSP, HSTS, and TLS configuration. These are one-time changes that eliminate whole vulnerability classes.4748## Best Practices4950- An ORM does not make you safe from SQL injection if you use its raw-query escape hatch with an f-string.51- Never log a password, token, session ID, or full card number — including in an exception's message or a request dump.52- Comparing secrets with `==` leaks their length and content through timing. Use a constant-time comparison.53- A JWT with `alg: none` accepted by your verifier is a full authentication bypass. Pin the algorithm; never trust the header's claim about it.54- Validate on an allowlist, not a denylist. You cannot enumerate every malicious input, but you can enumerate every valid one.55- Rate-limit authentication endpoints. Without it, a leaked password list is a working login.5657## Examples5859**The vulnerability that route-level auth does not catch:**6061```python62# The route requires a login. It does not check that the order is the user's.63@router.get("/orders/{order_id}")64@requires_auth # authenticated, but not authorized65async def get_order(order_id: str, user: User = Depends(current_user)):66 return await db.orders.get(order_id) # any user can read any order6768# Correct: authorization is a property of the object, not the route.69@router.get("/orders/{order_id}")70async def get_order(order_id: str, user: User = Depends(current_user)):71 order = await db.orders.get(order_id)72 if order is None or order.customer_id != user.id:73 # Same response for "does not exist" and "not yours": do not leak existence.74 raise HTTPException(404, "Order not found")75 return order76```7778**Passwords and token comparison:**7980```python81from argon2 import PasswordHasher82from argon2.exceptions import VerifyMismatchError83import hmac8485ph = PasswordHasher() # Argon2id, correct parameters by default8687def hash_password(plain: str) -> str:88 return ph.hash(plain) # salt is generated and embedded8990def verify_password(plain: str, stored: str) -> bool:91 try:92 ph.verify(stored, plain)93 return True94 except VerifyMismatchError:95 return False9697def verify_webhook(signature: str, expected: str) -> bool:98 # `==` on secrets leaks information through timing. This does not.99 return hmac.compare_digest(signature, expected)100```101102**Parameterized, always:**103104```python105# Injectable. The ORM does not save you here.106await db.execute(f"SELECT * FROM orders WHERE status = '{status}'")107108# Safe.109await db.execute("SELECT * FROM orders WHERE status = :status", {"status": status})110```111112## Notes113114- Broken object-level authorization (BOLA/IDOR) is consistently the top API vulnerability. Every endpoint that takes an ID must check that the caller may access *that* object. A route guard is not enough.115- Returning 404 rather than 403 for objects the user may not access prevents enumeration — a 403 confirms the object exists.116- Argon2id is the current recommendation for password hashing. bcrypt remains acceptable. PBKDF2 is acceptable where required by compliance. A bare hash — even SHA-256 — is not password hashing, it is a lookup table waiting to be built.