Secure Coding Standard Template
Secure coding rules by area with do/don't code examples, SAST/DAST/secret scanning tooling and an exceptions process
How to use: Use to set the minimum secure coding rules for a team or organisation. Fill scope and languages first, then replace the examples with code from your own stack. The main pitfall is rules that no tool or reviewer checks; map each rule area to a tool in the tooling section.
Preview
Secure Coding Standard: [Organisation or product]
Secure coding standard for application developers: scope and languages, mandatory rules by area (input validation, output encoding, authentication, session management, access control, cryptography, secrets, error handling and logging, dependency use, file handling, API security) with do and don't code examples, required tooling (SAST, DAST, secret scanning) and an exceptions process. The areas reflect widely used OWASP concepts (for example the OWASP Top 10 and ASVS topics); the rules and examples are original. Replace every
[bracketed]value and adapt examples to your stack.
| Field | Value |
|---|---|
| Document ID | [SCS-STD-001] |
| Version | [1.0] |
| Status | [Draft / In review / Approved] |
| Owner | [@appsec-lead] |
| Approvers | [@engineering-director], [@ciso] |
| Applies to | [All production code in repositories under github.com/[org]] |
| Effective date | YYYY-MM-DD |
| Next review | YYYY-MM-DD |
1. Purpose and scope
This standard defines the minimum secure coding rules for software written or maintained by [Organisation]. It applies to production code, infrastructure as code and build scripts. Prototypes that never handle real data are exempt until they are promoted.
| Language / stack | Frameworks | Example section owner |
|---|---|---|
| Example: TypeScript (Node.js 22 LTS) | Express, React | [@web-guild] |
| Example: Python 3.12 | FastAPI, SQLAlchemy | [@python-guild] |
| Example: Go 1.23 | net/http, sqlc | [@platform] |
| [Language] | [Frameworks] | [@owner] |
Rule keywords: MUST is mandatory and enforced in review; SHOULD is expected unless a documented reason applies.
2. Input validation
| ID | Rule |
|---|---|
| IV-01 | Input from any source outside the process (HTTP, queues, files, environment) MUST be validated on the server against a schema. |
| IV-02 | Validation MUST use allow-lists (expected type, length, pattern, range), not block-lists of bad characters. |
| IV-03 | Unexpected fields SHOULD be rejected, and MUST NOT be bound automatically to internal objects (mass assignment). |
Do:
import { z } from "zod";
const CreateOrder = z.object({
sku: z.string().regex(/^[A-Z0-9-]{3,32}$/),
quantity: z.number().int().min(1).max(999),
}).strict(); // rejects unknown fields
const input = CreateOrder.parse(req.body);
Don't:
// Trusts every field the client sends, including "price" and "isAdmin"
const order = await db.order.create({ data: req.body });
3. Output encoding and injection prevention
| ID | Rule |
|---|---|
| OE-01 | Database queries MUST use parameterised statements or a query builder; string concatenation of input into queries is forbidden. |
| OE-02 | HTML output MUST be encoded by the template engine; raw HTML insertion requires sanitisation with an approved library. |
| OE-03 | Operating system commands MUST NOT be built from input; use library calls or argument arrays without a shell. |
| OE-04 | Dynamic code execution (eval, Function, expression languages) MUST NOT process input. |
Do:
cur.execute("SELECT id, total FROM orders WHERE customer_id = %s", (customer_id,))
subprocess.run(["convert", src_path, dst_path], check=True) # no shell
Don't:
cur.execute(f"SELECT * FROM orders WHERE customer_id = '{customer_id}'")
os.system("convert " + filename + " out.png")
4. Authentication
| ID | Rule |
|---|---|
| AU-01 | User authentication MUST go through [corporate IdP via OIDC]; custom login forms are not permitted for staff applications. |
| AU-02 | Where local passwords are unavoidable they MUST be hashed with [Argon2id] using library defaults or stronger, never with fast hashes. |
| AU-03 | Login, recovery and MFA endpoints MUST be rate limited and return the same response for unknown and known accounts. |
| AU-04 | Service-to-service calls MUST authenticate with workload identity or short-lived tokens, not shared static passwords. |
Do:
from argon2 import PasswordHasher
ph = PasswordHasher()
stored = ph.hash(password)
ph.verify(stored, attempt)
Don't:
stored = hashlib.md5(password.encode()).hexdigest()
5. Session management
| ID | Rule |
|---|---|
| SE-01 | Session cookies MUST set Secure, HttpOnly and SameSite=Lax or Strict, and SHOULD use the __Host- prefix. |
| SE-02 | Session identifiers MUST be regenerated after login and after privilege change. |
| SE-03 | Logout MUST invalidate the session on the server, not only delete the cookie. |
| SE-04 | Browser applications SHOULD NOT keep access or refresh tokens in localStorage. |
Do:
res.cookie("__Host-sid", sessionId, {
secure: true, httpOnly: true, sameSite: "lax", path: "/", maxAge: 30 * 60 * 1000,
});
Don't:
localStorage.setItem("refreshToken", token); // readable by any injected script
6. Access control
| ID | Rule |
|---|---|
| AC-01 | Every endpoint MUST perform an authorization check on the server; the default MUST be deny. |
| AC-02 | Object access MUST verify the caller's relationship to the specific record (ownership, tenant, role), not only that the caller is logged in. |
| AC-03 | Authorization logic SHOULD live in one shared module or policy engine, not repeated per handler. |
Do:
order, err := store.GetOrder(ctx, orderID)
if err != nil || order.TenantID != caller.TenantID {
http.NotFound(w, r) // do not reveal existence
return
}
Don't:
order, _ := store.GetOrder(ctx, r.URL.Query().Get("id")) // any logged-in user, any order
7. Cryptography
| ID | Rule |
|---|---|
| CR-01 | Only algorithms on the approved list MUST be used: [AES-256-GCM, ChaCha20-Poly1305, SHA-256 or stronger, Ed25519, ECDSA P-256, RSA 3072+]. |
| CR-02 | ECB mode, MD5 and SHA-1 for security purposes, and home-made schemes MUST NOT be used. |
| CR-03 | Random values for tokens, keys and IDs MUST come from a cryptographically secure generator. |
| CR-04 | Keys MUST be managed in [KMS / HSM]; application code receives handles or data keys, never master keys. |
Do:
import { randomBytes } from "node:crypto";
const resetToken = randomBytes(32).toString("base64url");
Don't:
const resetToken = Math.random().toString(36).slice(2); // predictable
8. Secrets
| ID | Rule |
|---|---|
| SK-01 | Secrets MUST NOT be committed to repositories, including tests and history; secret scanning with push protection MUST be enabled. |
| SK-02 | Applications MUST read secrets at runtime from [secrets manager]; container images MUST NOT contain secrets. |
| SK-03 | A leaked secret MUST be revoked and rotated within [4 hours]; deleting the commit is not sufficient. |
Do:
db_password = secrets_client.get_secret_value(SecretId="prod/payments/db")["SecretString"]
Don't:
DB_PASSWORD = "Summer2026!" # in source, in history, in every clone
9. Error handling and logging
| ID | Rule |
|---|---|
| EL-01 | Errors returned to clients MUST be generic and carry a correlation ID; stack traces and queries MUST NOT reach clients. |
| EL-02 | Security events (login success and failure, access denied, privilege change, data export) MUST be logged with who, what, when and from where. |
| EL-03 | Logs MUST NOT contain passwords, tokens, full card numbers or unnecessary personal data. |
| EL-04 | Log entries built from input MUST be structured (JSON fields), not concatenated strings, to prevent log injection. |
| EL-05 | On error in a security check the code MUST deny access (fail closed). |
Do:
logger.warn({ event: "authz_denied", userId, resource: "order", orderId, requestId });
res.status(404).json({ type: "about:blank", title: "Not found", requestId });
Don't:
logger.info("Login for " + req.body.email + " with password " + req.body.password);
res.status(500).send(err.stack);
10. Dependency use
| ID | Rule |
|---|---|
| DE-01 | New dependencies MUST pass the intake review in [link to intake process]. |
| DE-02 | Lockfiles MUST be committed and CI MUST install from them. |
| DE-03 | Dependencies with known critical or high vulnerabilities MUST be updated within [7 / 30] days or covered by an exception. |
| DE-04 | CI actions and base images SHOULD be pinned by commit SHA or digest. |
Do:
- uses: actions/checkout@<full-40-character-commit-sha> # v4.2.2
- run: npm ci
Don't:
- uses: some-org/deploy-action@main
- run: npm install
11. File handling
| ID | Rule |
|---|---|
| FH-01 | Uploads MUST be limited in size and validated by content (magic bytes), not only by extension or client content type. |
| FH-02 | Stored files MUST use server-generated names; user-supplied paths MUST NOT reach file system calls. |
| FH-03 | Archives MUST be extracted with checks against path traversal and decompression bombs. |
| FH-04 | Downloads of user files SHOULD be served with Content-Disposition: attachment and X-Content-Type-Options: nosniff. |
Do:
safe_name = f"{uuid4()}.pdf"
dest = (UPLOAD_ROOT / safe_name).resolve()
assert dest.is_relative_to(UPLOAD_ROOT)
Don't:
open(os.path.join("/srv/uploads", request.args["name"]), "wb").write(data) # ../../etc
12. API security
| ID | Rule |
|---|---|
| AP-01 | Every API MUST have an OpenAPI or GraphQL schema that is enforced at runtime. |
| AP-02 | Responses MUST return only the fields the client needs; internal fields MUST be excluded by an explicit response model. |
| AP-03 | APIs MUST apply rate limits per client and limits on page size, query depth and request body size. |
| AP-04 | CORS MUST allow only named origins; wildcard origins with credentials are forbidden. |
| AP-05 | Server-side requests to user-supplied URLs MUST go through an allow-list and block internal address ranges. |
Do:
class OrderOut(BaseModel):
id: str
status: str
total: Decimal # no internal cost, no customer email
@app.get("/orders/{order_id}", response_model=OrderOut)
Don't:
@app.get("/orders/{order_id}")
def get_order(order_id: str):
return db.query(Order).get(order_id).__dict__ # every column, every tenant
13. Tooling and verification
| Tool type | Tool | Runs | Blocking threshold | Rules covered |
|---|---|---|---|---|
| SAST | Example: CodeQL, Semgrep with org ruleset | Every pull request | New high or critical finding | IV, OE, CR, FH |
| Secret scanning | Example: Platform secret scanning with push protection; gitleaks in CI | Every push | Any verified secret | SK |
| Dependency scanning (SCA) | Example: Dependabot alerts, OSV-Scanner | Daily and on PR | Critical or high with fix available | DE |
| DAST | Example: OWASP ZAP baseline scan | Nightly on staging | High alerts | SE, AP, EL |
| Infrastructure as code scanning | [Tool] | Every PR | [Threshold] | [Rules] |
| Code review checklist | Pull request template | Every PR | Reviewer sign-off | All |
Coverage target: [100%] of production repositories run SAST, secret scanning and SCA by YYYY-MM-DD.
14. Exceptions process
A team that cannot meet a MUST rule requests an exception before merge:
- Open a ticket in [SEC project] with the rule ID, the code location, the reason and the compensating controls.
- The application security team assesses risk within [5 business days].
- Approval authority: Medium risk by [@appsec-lead]; High risk by [@ciso].
- Approved exceptions are recorded in the register below, linked from a code comment at the location, and expire after at most [6 months].
| ID | Rule | Repository / location | Reason | Compensating control | Approved by | Expires |
|---|---|---|---|---|---|---|
| EX-001 | Example: CR-02 | legacy-sync/hash.py | Partner API requires SHA-1 HMAC | Isolated service, partner IP allow-list, migration planned | [@appsec-lead] | YYYY-MM-DD |
| [EX-nnn] | [Rule] | [Location] | [Reason] | [Control] | [@name] | YYYY-MM-DD |
15. Training and review
New developers complete [secure coding training] within 30 days of joining and yearly afterwards. Compliance is measured quarterly by [open SAST findings per repository, exceptions past expiry, and the share of repositories with all required tools]. The standard owner reviews this document yearly and after any incident caused by a coding weakness.
Standard and licence note
Based on common industry practice for secure coding and on widely used OWASP concepts, such as the risk areas of the OWASP Top 10 and the topic areas of the OWASP Application Security Verification Standard. No OWASP text is reproduced; the rules, IDs and code examples are original and written for this collection. Consult OWASP resources (owasp.org) for detailed guidance. Template licence: MIT.
Related templates
Coding Standards Template
Team coding standards: naming, tool-enforced formatting, structure, errors, logging, tests, reviews, language rules
Software & EngineeringArchitecture Decision Record (ADR) Template
Lightweight Nygard-style ADR: status, context, decision, consequences, alternatives, related decisions, lifecycle and log
Software & EngineeringAGENTS.md Template for AI Coding Agents
Repository instructions for AI coding agents: setup, build and test commands, conventions, architecture map, PR and security rules