Skip to main content

Secure Coding Practices

Secure coding is the discipline of building software so that common attacks fail by design. It covers input handling, authorization, secrets, dependencies, CI/CD, logging, and the release process. Recent supply chain incidents show that secure coding no longer stops at application code. Your packages, editor extensions, build scripts, tokens, and deployment pipeline are also part of the product’s security boundary.

1. Input Validation

Never trust data from users, clients, webhooks, files, APIs, queues, or third-party systems. Every input can be an attack path:
  • Form fields
  • URL parameters
  • HTTP headers
  • Cookies
  • File uploads
  • API payloads
  • Webhook events
  • CSV imports
  • AI tool outputs
  • Messages from queues or workers

Validation Strategies

StrategyDescriptionExample
AllowlistingDefine exactly what is allowedUsername matches ^[a-zA-Z0-9_]{3,20}$
Type checkingConfirm expected typeage must be an integer
Length limitsRestrict sizeComment maximum 5000 characters
Range checksVerify numerical boundariesQuantity must be 1 to 100
Schema validationValidate complete object structureAPI payload must match a JSON schema
Prefer allowlists over blocklists. Attackers can bypass blocklists with encoding tricks, Unicode confusables, alternate syntax, and unexpected parser behavior.

2. Output Encoding

Output encoding prevents untrusted data from becoming executable content in a browser.
Output contextDefense
HTML bodyHTML entity encoding
HTML attributeAttribute encoding
JavaScript stringJavaScript encoding or avoid inline JS
URL parameterPercent encoding
CSSCSS escaping or avoid untrusted CSS
Modern frameworks such as React, Angular, Vue, and Svelte escape output by default in normal templates. The risk returns when developers bypass those protections.
If you use APIs such as dangerouslySetInnerHTML, innerHTML, or v-html, sanitize first with a trusted HTML sanitizer such as DOMPurify.

3. Parameterized Queries

Parameterized queries separate SQL code from user data.
# Vulnerable: user input changes query syntax
query = f"SELECT * FROM users WHERE email = '{email}'"

# Safer: user input is passed as data
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
// Vulnerable
const query = `SELECT * FROM users WHERE id = ${userId}`;

// Safer with node-postgres
const result = await pool.query(
  "SELECT * FROM users WHERE id = $1",
  [userId]
);
ORMs help, but raw query APIs can still be dangerous. Treat raw SQL as a sharp tool.

4. Authorization by Default

Authentication asks “who are you?” Authorization asks “what are you allowed to do?” Secure systems enforce authorization on the server for every sensitive object and action.

Patterns

  • Deny by default.
  • Centralize authorization checks where practical.
  • Check object ownership, not just route access.
  • Use least privilege roles.
  • Re-check authorization after state changes.
  • Log authorization failures.

Anti-Pattern

Hiding an admin button in the UI but leaving the API endpoint unprotected is not authorization. Attackers can call the API directly.

5. Secrets Management

Secrets include passwords, tokens, API keys, SSH private keys, signing keys, database URLs, webhooks secrets, and cloud credentials.

Where Secrets Belong

EnvironmentBetter storage
Local development.env files excluded from Git, or local secret stores
CI/CDPlatform secret storage with least privilege
ProductionCloud KMS, managed secret manager, Vault, or platform equivalent
KubernetesExternal secrets operators or encrypted secret storage
Developer machinesPassword manager, SSH agent, hardware key, or short-lived credentials

Secret Hygiene

  • Never hardcode secrets.
  • Never commit .env files.
  • Scan commits with tools such as gitleaks, trufflehog, or detect-secrets.
  • Use short-lived credentials where possible.
  • Scope tokens narrowly.
  • Rotate secrets after exposure.
  • Separate development, staging, and production secrets.
In a developer tool compromise, assume secrets available to that tool may be exposed. Rotate GitHub, package registry, cloud, SSH, CI/CD, and Vault tokens as needed.

6. Dependency Security

Modern applications depend on open-source packages, containers, build plugins, editor extensions, and SaaS SDKs. Each one can introduce vulnerabilities or malicious behavior.

Supply Chain Risks

  • Known vulnerable dependency versions.
  • Typosquatted packages.
  • Compromised maintainer accounts.
  • Malicious install scripts.
  • Dependency confusion between public and private package names.
  • Auto-updates that pull unreviewed code.
  • Abandoned packages with no security fixes.

Controls

  • Use lockfiles for reproducible installs.
  • Review dependency diffs in pull requests.
  • Run dependency scanning in CI.
  • Remove unused packages.
  • Prefer packages with active maintainers and clear ownership.
  • Use private package namespaces to reduce dependency confusion.
  • Restrict install scripts where possible.
  • Monitor package publishes for unexpected releases.

7. Software Provenance and Build Integrity

Software provenance answers: where did this artifact come from, what source produced it, and what workflow built it?

Practices

  • Use protected branches.
  • Require code review for critical paths.
  • Require signed commits or signed releases where appropriate.
  • Generate SBOMs for releases.
  • Produce build attestations for important artifacts.
  • Keep build logs.
  • Isolate CI runners.
  • Do not expose production secrets to untrusted pull request workflows.
  • Pin third-party CI actions to specific versions or commits.
Cryptographic signatures prove that a key signed something. They do not prove the signer, build system, or maintainer account was not compromised. Combine signing with access control, review, monitoring, and incident response.

8. CI/CD Security

CI/CD systems are high-value targets because they can access source code, secrets, deployments, containers, cloud accounts, and package registries.

CI/CD Controls

  • Require branch protection on main branches.
  • Use least-privilege service accounts.
  • Separate build, test, staging, and production permissions.
  • Avoid long-lived deployment tokens.
  • Restrict who can modify workflows.
  • Protect self-hosted runners.
  • Require manual approval for production deploys when risk is high.
  • Log secret access and deployment activity.
  • Scan artifacts before release.

Pull Request Safety

Be careful when CI runs code from forks or untrusted contributors. That code may try to print secrets, modify workflows, or abuse runner permissions. Safer options:
  • Do not expose secrets to untrusted PRs.
  • Use read-only tokens for validation jobs.
  • Require maintainer approval before running privileged jobs.
  • Review workflow changes carefully.

9. Secure Error Handling

Applications should fail safely and avoid leaking internal details.
# Bad: leaks internals to users
except Exception as e:
    return {"error": str(e)}

# Better: log details server-side, return a generic message
except Exception as e:
    logger.exception("Database operation failed")
    return {"error": "An internal error occurred. Please try again later."}
Principles:
  • Never show stack traces to end users.
  • Do not return database URLs, file paths, secrets, or internal hostnames.
  • Log enough detail for investigation.
  • Use correlation IDs so support can connect user reports to logs.
  • Fail closed for authentication and authorization errors.

10. Security Logging and Detection

Secure coding includes creating the evidence defenders need. Log security-relevant events:
  • Login success and failure
  • MFA enrollment and disablement
  • Password reset requests and completion
  • Session revocation
  • Role changes
  • Permission changes
  • API key creation and use
  • OAuth app approvals
  • Data exports
  • Webhook failures
  • Package publish events
  • CI/CD workflow changes
Avoid logging:
  • Passwords
  • Session cookies
  • MFA codes
  • API tokens
  • Full payment details
  • Sensitive personal data unless there is a clear need and protection

11. Threat Modeling

Threat modeling identifies what can go wrong before code is shipped. Use STRIDE:
LetterThreatQuestion
SSpoofingCan someone pretend to be another user or service?
TTamperingCan someone modify data, code, or configuration?
RRepudiationCan someone deny performing an action?
IInformation disclosureCan sensitive data leak?
DDenial of serviceCan the system be made unavailable?
EElevation of privilegeCan someone gain more access than intended?
Apply STRIDE to:
  • Authentication flows
  • Admin panels
  • Data export features
  • Webhooks
  • AI agent tools
  • CI/CD workflows
  • Package publishing
  • Third-party integrations

Secure Coding Checklist

  • Validate input server-side.
  • Encode output by context.
  • Use parameterized queries.
  • Enforce authorization on the server.
  • Store secrets outside code.
  • Use least-privilege tokens.
  • Scan for secrets before commit.
  • Review dependency changes.
  • Protect CI/CD workflows and runners.
  • Log security events.
  • Threat model sensitive workflows.

Practical Lab Ideas

  1. Add server-side validation to an API route.
  2. Fix an XSS bug by using contextual output encoding.
  3. Replace a vulnerable SQL query with a parameterized query.
  4. Add object-level authorization to an endpoint.
  5. Run a secret scanner on a sample repository.
  6. Create an SBOM and inspect transitive dependencies.
  7. Harden a CI workflow so untrusted PRs cannot access secrets.
  8. Threat model a package publishing workflow.

Key Takeaways

  1. Secure coding includes the build and release path.
  2. Authorization must be server-side and object-aware.
  3. Secrets need lifecycle management, not just hiding.
  4. Dependencies and extensions are attack surface.
  5. CI/CD deserves the same security attention as production.
  6. Good logs make response possible.
  7. Threat modeling catches missing controls before attackers do.