Project
Secure Design Patterns
February 2023
I wrote these notes and the accompanying Java examples while preparing for a software security exam. The goal was to understand six patterns from the Software Engineering Institute’s Secure Design Patterns report well enough to implement them from scratch. Each example is a few small classes you can compile and run in seconds.
The idea behind all six
Classic design patterns such as Factory, Strategy or Visitor are about structure. The secure variants reuse that structure to answer one question: where does the security decision happen, and can the rest of the code get around it?
The answer is the same in every pattern. Put the check in one place that sits on the only path to the protected thing, the object, the algorithm or the operation, so callers cannot skip it. That place becomes easy to review and test, and changing the policy means changing one class instead of hunting through call sites.
| Pattern | Where the check lives | Protects |
|---|---|---|
| Secure Factory | Factory, before an object is created | Access to sensitive objects |
| Secure Strategy Factory | Factory, when an algorithm is chosen | Which algorithm a caller may use |
| Secure Builder Factory | Factory, when a builder is handed out | Who may set privileged fields |
| Secure Chain of Responsibility | Each handler in a pipeline | A request, step by step |
| Secure State Machine | Each state’s transition logic | Which actions are allowed when |
| Secure Visitor | The visitor for each role | Operations on protected data |
Secure Factory
Problem: if any part of the code can instantiate a database connection directly, every call site has to remember to check permissions.
How it works: clients never call constructors. They ask a factory, which checks the caller’s credentials and only then returns an object behind an interface. The factory is the single enforcement point.
In the repo: DatabaseFactoryConnection and APIFactoryConnection return MySQL, PostgreSQL, REST or SOAP connections based on the requested type. Callers only ever see the DatabaseConnection or APIConnection interface.
Watch out for: a factory that returns null or a default object when a request is not allowed. Callers then have to check for it, and the ones that forget fail somewhere else. Refusing loudly, with an exception, keeps the failure at the enforcement point.
Secure Strategy Factory
Problem: several implementations of the same task exist with different security strength, for example password, token and biometric authentication, and the caller should not be able to pick a weaker one than allowed.
How it works: a factory looks at the caller’s credentials and returns the strategy that caller may use. The calling code only sees the Strategy interface.
In the repo: PasswordStrategyFactory, TokenStrategyFactory and BiometricStrategyFactory each hand out one strategy, and a Context runs it.
Watch out for: silent downgrades. If a caller asks for the strong mechanism and quietly receives the weak one, that should at least be logged, or the request should be rejected.
Secure Builder Factory
Problem: complex objects like user accounts contain privileged fields such as roles or permissions. A general-purpose builder would let anyone set them.
How it works: a factory hands out a builder based on the caller’s role, and only the privileged builder can set privileged fields. A director calls the build steps in a fixed order, so no half-built object escapes.
In the repo: AdminBuilderFactory and UserBuilderFactory return an AdminBuilder or UserBuilder, and the Director assembles the Account.
Watch out for: setters on the finished object. If Account exposes a public setter for its role, the builder protection is gone.
Secure Chain of Responsibility
Problem: a request needs several independent checks, such as logging, authentication and authorization, and each should stay small and replaceable.
How it works: handlers form a chain. Each performs one check and either forwards the request or stops it. The core of the repo example is only a few lines:
public void handleRequest(Credentials credentials) {
if (check(credentials)) {
if (successor != null) {
successor.handleRequest(credentials);
}
} else {
System.out.println("Request denied by " + this.getClass().getSimpleName());
}
}
In the repo: LoggingHandler, then AuthenticationHandler, then AuthorizationHandler. The same structure is common in practice: servlet filter chains and middleware pipelines in web frameworks work this way.
Watch out for: order and completeness. Authorization before authentication is meaningless, and a chain that someone can call starting from the middle skips the earlier checks.
Secure State Machine
Problem: what a user may do depends on where they are in a process: not logged in, logged in, authorized. Scattered if (loggedIn && hasRole ...) checks drift apart over time.
How it works: each state is a class that decides which transitions are allowed. A context object holds the current state and delegates every request to it. An action that is not allowed in the current state simply has no transition.
In the repo: UnauthenticatedState moves to AuthenticatedState only with valid credentials, which moves to AuthorizedState only with the required role. SecurityContext always starts in the unauthenticated state.
Watch out for: a public setState method. In the example, SecurityContext.setState is public so the states can call it, which also means any other code could jump straight to the authorized state. In a real system the transition method should not be reachable from outside.
Secure Visitor
Problem: protected data needs different operations for different roles, and adding role logic to every data class spreads access checks everywhere.
How it works: the data classes only accept visitors. Each visitor represents a role and decides what that role may do with each kind of element, so new roles or operations mean new visitors, not changes to the data classes.
In the repo: GuestVisitor and AdminVisitor visit LockedContent, which requires a password, and UnlockedContent, which always opens. Both visitors ask the content to unlock itself with the caller’s credentials, so in this version the check lives in the content classes rather than in role-specific visitors.
Watch out for: data classes that also expose their contents through ordinary getters. In the example, getContent() is public, so any code can read locked content without going through a visitor at all. Then the visitor is decoration, not protection.
What the examples leave out
The code is meant for learning, so it simplifies on purpose:
- Credentials are simple values or flags. Real authentication would check against a user store with hashed passwords, tokens or an identity provider.
- Failures are printed, not raised. Production code should fail closed with exceptions and audit logging.
- There are no tests. For security logic, tests that prove a denied request really is denied are the most valuable ones.
A good exercise is to take one example and fix one of these gaps, for instance making the Secure Factory throw on unknown connection types and adding a test for it.
References
- Dougherty, C., Sayre, K., Seacord, R. C., Svoboda, D., and Togashi, K. (2009). Secure Design Patterns. Software Engineering Institute, Carnegie Mellon University.
- Gamma, E., Helm, R., Johnson, R., and Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Refactoring.Guru: Design Patterns, clear explanations of the underlying classic patterns.
- OWASP Secure Coding Practices Quick Reference Guide.