Software usually becomes insecure one reasonable-looking decision at a time. A team trusts an identifier supplied by the browser, leaves a debug setting enabled, postpones a dependency update, or assumes an internal endpoint will stay internal. None of those choices looks like a dramatic breach scene. Together, they can create one.

Application security is the work of finding and reducing those exploitable mistakes throughout a product’s life. A scanner near release is useful, but it cannot recover every design decision, authorization boundary, dependency, and operational assumption made months earlier.

The current OWASP Top 10:2025 is a useful awareness list, not a complete application-security standard. It covers broken access control, security misconfiguration, software supply-chain failures, cryptographic failures, injection, insecure design, authentication failures, software or data integrity failures, security logging and alerting failures, and mishandling of exceptional conditions.

Begin with the Design

Before implementation, identify:

  • valuable data and operations;
  • trust boundaries and data flows;
  • user and workload roles;
  • abuse cases, not only ordinary use cases;
  • authentication, authorization, and recovery paths;
  • external services and software dependencies;
  • logging, privacy, and availability requirements.

Threat modeling can reveal that a refund lacks a second approval or that one tenant can guess another tenant’s object ID. Those are design flaws; a code scanner may never understand the business rule.

Broken Access Control

Access control must be enforced on the server for every protected operation and object.

# The exact framework will differ; the important part is the server-side check.
invoice = repository.get_invoice(invoice_id)
if invoice.account_id != current_user.account_id:
raise Forbidden()
return invoice

Do not trust a hidden field, disabled button, client-supplied role, or predictable URL. Test horizontal access between users at the same privilege level and vertical access from an ordinary user to administrative actions.

Injection

Injection occurs when untrusted data is interpreted as part of a command or query.

Use parameterized database queries:

cursor.execute(
"SELECT id, email FROM users WHERE email = %s",
(submitted_email,),
)

Do not try to make SQL safe by removing quotes or “bad characters.” Similar rules apply to shell commands, LDAP filters, template languages, and expression evaluators: keep data separate from instructions and use the API designed for that context.

Avoid invoking a shell when a process API can accept an argument array. If an input must select a command or option, map it from a small allowlist rather than passing it through.

Cross-Site Scripting

Cross-site scripting (XSS) is a form of injection in which attacker-controlled content becomes executable browser code. The defense is contextual output encoding plus safe APIs, not one generic sanitization pass.

  • use framework templates that escape by default;
  • write text through textContent, not innerHTML;
  • sanitize HTML with a maintained allowlist sanitizer only when HTML input is genuinely required;
  • avoid constructing JavaScript, CSS, or URLs through string concatenation;
  • deploy a tested Content Security Policy as an additional layer.

An HttpOnly session cookie cannot be read by ordinary injected JavaScript, but XSS may still perform actions as the user. HTTPS protects traffic in transit; it does not prevent XSS.

Authentication and Sessions

Use maintained identity libraries or providers rather than designing a password protocol. Protect sessions with:

  • high-entropy identifiers;
  • Secure, HttpOnly, and suitable SameSite cookie attributes;
  • rotation after login or privilege changes;
  • server-side revocation;
  • bounded idle and absolute lifetime;
  • reauthentication for sensitive operations;
  • CSRF defenses where browser credentials are sent automatically.

Store passwords with Argon2id, scrypt, bcrypt, or an appropriately configured PBKDF2—not a fast generic hash.

Cryptographic Failures

Classify data before deciding what to encrypt. Use TLS for transport, AEAD for application-managed ciphertext, and a managed key service where possible. Do not log secrets, full tokens, passwords, or unnecessary personal data.

Encryption does not fix overcollection. Data that is no longer needed should be deleted according to a retention policy.

Dependencies and the Software Supply Chain

Applications include package-manager dependencies, container bases, build actions, compilers, plugins, and hosted services. Manage that chain by:

  • committing and reviewing lock files;
  • using trusted registries and scoped publisher permissions;
  • verifying signatures or provenance where the ecosystem supports them;
  • scanning source and deployed artifacts;
  • producing a software bill of materials (SBOM) when it serves an operational need;
  • removing abandoned packages;
  • protecting build credentials and runners;
  • testing updates rather than freezing vulnerable versions forever.

Software Composition Analysis (SCA) can identify known vulnerable components. It cannot prove that a component is safe or that the vulnerable path is reachable.

Secrets

Secret scanning can catch keys committed to source control, but prevention is better:

  • use workload identity and short-lived credentials;
  • keep development, test, and production secrets separate;
  • inject secrets at runtime rather than baking them into images;
  • redact logs and diagnostic bundles;
  • rotate an exposed secret—deleting it from the latest commit is not enough.

APIs

APIs need explicit schemas, authentication, object-level authorization, resource limits, and safe error handling. Validate structure, type, size, range, and business meaning at a trust boundary. Validation is not the same as output encoding; applications often need both at different points.

Rate limits reduce abuse but should be tied to the relevant identity, resource, and cost. A global requests-per-second limit may not stop one account from exporting every record it is incorrectly allowed to read.

Safe Failure

Exceptional conditions deserve design work. A timeout, partial payment, duplicate request, full disk, missing dependency, or failed authorization check should move the system into a known state.

Use idempotency keys for operations that may be retried, transactions where atomicity is required, bounded queues and inputs, and generic external error messages paired with useful internal diagnostics. Do not fail open because an authorization or policy service is unavailable unless that trade-off is explicit and justified.

Testing

A balanced program combines:

  • unit and integration tests for security invariants;
  • code review;
  • static analysis (SAST);
  • dependency and secret scanning;
  • dynamic testing (DAST);
  • fuzzing for parsers and complex input handling;
  • manual testing for business logic and chained flaws;
  • verification after remediation.

Tools produce evidence, not certainty. Tune them, assign owners, record accepted risk, and test whether fixes reached every supported release.

Logging and Alerting

Record security-relevant actions such as authentication outcomes, privilege changes, administrative operations, and high-value data exports. Include a trustworthy timestamp, actor, action, target, outcome, and correlation ID where appropriate.

Logs must not become another sensitive-data breach. Restrict access, protect integrity, define retention, and alert on patterns that someone can actually investigate.

Conclusion

Secure software comes from explicit boundaries and repeatable engineering habits. Parameterized queries, contextual encoding, strong sessions, controlled dependencies, and useful logs are concrete. “Sanitize everything” and “run a scanner” are not a security design.

References