This chapter offers practical guidance for applying secure-by-design principles to legacy codebases. It starts by urging teams to identify bounded contexts and semantic boundaries so that domain primitives can be introduced where they make the most sense. The authors outline common smells that frequently appear in older systems—ambiguous parameters, unchecked logging, defensive code constructs, misapplied DRY, insufficient validation in domain types, and a bias toward only “good path” tests—and show how to prioritize and address them without disrupting delivery.
Key refactorings revolve around replacing ambiguous parameters with explicit domain primitives or secure entities and choosing a fitting migration strategy: a direct swap in smaller codebases, a discovery-first approach to surface data-quality issues, or a new API that enables incremental adoption. The chapter warns against logging unchecked strings and highlights risks like data leakage and second-order injection, as well as subtle leaks via implicit toString usage or reflection; remedies include explicit accessors and treating sensitive values as read-once. It challenges defensive coding patterns—scattered null/format checks and overuse of Optional—advocating clear contracts and centralized validation. On DRY, it emphasizes deduplicating ideas, not text, to avoid overcoupled “util” dependencies, and it teaches how to spot both false positives and false negatives in duplication.
Further guidance strengthens domain types with strict, domain-informed invariants and elevates testing beyond normal and boundary cases to include invalid, extreme, and dependency-failure scenarios. The authors caution against partial domain primitives that miss key context, illustrating with money amounts that must carry currency to form a conceptual whole and prevent invalid operations. Throughout, the chapter balances rigor with pragmatism: introduce types where semantics are stable, expect data-quality issues to surface, and choose a migration path that fits the codebase size and team structure to steadily improve security by design in legacy systems.
Summary
- Introducing domain primitives should be done at the semantic boundary of your context.
- Ambiguous parameters in APIs are a common source of security bugs.
- You should be on the lookout for ambiguous parameters when reading code, and address those using the direct approach, the discovery approach, or the new API approach.
- Never log unchecked user input because it opens up the risk of second-order injection attacks.
- Limit the number of times a sensitive value can be accessed because it allows you to detect unintentional access.
- Use explicit accessor methods for data that you want to log. Otherwise, new fields can end up in logs by accident.
- Because defensive code constructs can be harmful, clarify those using contracts and domain primitives.
- The DRY principle (Don’t Repeat Yourself) is about repeated representation of knowledge, not about repeated text.
- Trying to reduce repeated text that isn’t repeated knowledge can cause unnecessary dependencies.
- Failing to reduce repeated knowledge because the text differs can cause vulnerable inconsistencies.
- Tests reveal possible weaknesses in your code, and you should look for invalid and extreme input tests.
- Ensure that your domain primitives encompass an entire conceptual whole.
- Be on the lookout for domain types lacking proper validation and address these with domain primitives and secure entities.
FAQ
How do I decide where to apply domain primitives in a legacy codebase?
Start by identifying a bounded context and its semantic boundary. Group concepts that share the same meaning (for example, everything that uses reservationId) and introduce domain primitives at that boundary. Avoid “just wrapping” strings/ints; a domain primitive must enforce the invariant and represent a complete concept within its context.Why are ambiguous parameter lists a security risk, and what’s the fix?
Generic and duplicate types make it easy to swap arguments and bypass validation (for example, int,int or two Address parameters). Replace them with domain primitives or secure entities so the signature is unambiguous and each value is validated at creation (ItemId, Quantity, BillingAddress, ShippingAddress).Which approach should I use to replace ambiguous parameters: direct, discovery, or new API?
- Direct approach: swap all parameters to domain types and fix breakages fast. Best for small codebases/one team; risky in large systems or with poor data quality.
- Discovery approach: keep the old API, attempt to create domain types internally, log failures, fix data issues, then change the API. Good when data quality is poor.
- New API approach: add a new hardened API alongside the old one, delegate/port logic, migrate callers incrementally, then deprecate/remove the old API. Works well for large/multi-team codebases; combine with discovery for bad data.
Secure by Design ebook for free