Why Security Engineers Prefer Simple Code Over Deep Abstraction
The Problem
I was reviewing a security-critical authentication system when I hit a wall. The code used deep inheritance hierarchies, abstract factories, and dependency injection. Tracing a single request required jumping through 12 files. I couldn’t answer the simple question: “Is this request validated before it reaches the database?”
That’s when I understood why security engineers prefer simple code. Deep abstraction doesn’t just make code harder to read. It creates hiding spots for vulnerabilities.
My Initial Misunderstanding
I used to think security engineers were behind the times. They seemed to reject modern patterns, prefer flat functions over elegant hierarchies, and avoid dependency injection frameworks. I assumed they just hadn’t learned better patterns.
I was wrong. They had learned those patterns and found them dangerous.
The Core Insight: Abstraction Hides Bugs
One security engineer (score: 38 on Reddit) put it bluntly:
“There’s a reason security stuff is mostly declarative, a very good one…”
Another developer (score: 1) added the key insight:
“A lot of security work values simple, explicit, easy-to-audit code over deep class hierarchies. Security engineers understand that deep abstraction and complexity hides more bugs than anything else.”
Let me show you what this means in practice.
What Goes Wrong: A Real Example
I wrote what I thought was clean, pattern-following code for request handling:
abstract class BaseHandler { protected abstract void process(Request req); protected void validate(Request req) { if (req.data == null) throw new ValidationException(); } public void handle(Request req) { validate(req); // Is this always called? process(req); }}This looked reasonable. The base class guarantees validation. But then a teammate added:
class BypassHandler extends BaseHandler { @Override public void handle(Request req) { // SECURITY BUG: Skips validation by not calling super process(req); // Direct call, validate() bypassed! }}The bug was invisible in code review. We only found it during a security audit when someone noticed unvalidated data reaching the database. The abstraction gave us false confidence.
Why This Matters for Security Audits
Security code must answer a simple question: “Can untrusted input reach a sensitive operation without validation?”
In complex OOP code, answering this requires:
1. Find all subclasses of BaseHandler2. Check each one's handle() method3. Verify which call super.handle()4. Check if validate() is overridden5. Trace virtual dispatch possibilities6. Consider future subclasses (unknown unknowns)Compare to simple, explicit code:
func handleRequest(req *Request) error { // Step 1: Validate (explicit, cannot be bypassed) if err := validate(req); err != nil { return fmt.Errorf("validation failed: %w", err) }
// Step 2: Sanitize (visible in control flow) sanitized := sanitize(req.Data)
// Step 3: Process (no hidden behavior) return process(sanitized)}One glance tells you: validation happens before processing. No subclasses to check. No virtual dispatch to trace.
The Attack Surface Problem
Another developer (score: 20) highlighted two key benefits:
“Minimal dependencies (less attack surface), Readability for the next person who has to audit it.”
Let’s compare attack surfaces:
Complex OOP:├── AbstractBaseHandler (1 class)│ ├── AbstractSecureHandler (1 class)│ │ ├── ConcreteHandlerA (1 class)│ │ └── ConcreteHandlerB (1 class)├── DependencyInjector (1 class)│ ├── Container (1 class)│ └── 50+ transitive dependencies└── ORMMapper (1 class + 10 plugins)
Total: ~70 components to audit
Simple Code:├── handlers/│ ├── auth.go (1 file, 150 lines)│ └── process.go (1 file, 200 lines)└── vendor/ └── 5 vetted dependencies
Total: 7 files, ~1000 lines to auditEach component is a potential attack vector. More components = more places for bugs to hide.
My Journey: From Annoyance to Appreciation
A developer (score: 2) described my exact experience:
“The simplicity that annoyed me at first ended up being the thing I liked most about it for security work, less abstraction means fewer places for bugs to hide.”
I went through phases:
- Week 1: “This code is primitive. Where are the patterns?”
- Week 2: “I guess it’s easier to understand, but it’s repetitive”
- Month 1: “Oh, I can audit this entire file in 10 minutes”
- Month 3: “I found a critical bug because the code was obvious”
Now I write simple code by default and add abstraction only when necessary.
The Declarative Pattern: Security’s Preferred Approach
Security tools overwhelmingly use declarative approaches. Look at firewall rules:
rules: - allow: port 443 from 0.0.0.0/0 - deny: allOr access control:
roles: admin: permissions: [read, write, delete] viewer: permissions: [read]Why? Because declarative code:
- Has finite, enumerable states
- Has no hidden control flow
- Can be audited without execution
- Is immutable by default
Common Mistakes: Over-Engineering Security
I’ve made all these mistakes:
Mistake 1: Burying security logic in inheritance
// Wrong: Security logic buried 3 layers deepclass SecurityManager extends AbstractSecurityBase extends BaseComponent implements ISecurity { // Good luck finding the actual validation logic}Mistake 2: Pattern overkill for simple tasks
// Wrong: 100 lines to validate an emailinterface IValidator<T> { ValidationResult validate(T input);}
abstract class AbstractValidator<T> implements IValidator<T> { protected abstract ValidationRule<T> getRule(); // ... 5 more abstract methods}
class EmailValidator extends AbstractValidator<String> { // Why is this so complicated?}The right approach:
// Right: Simple, auditable, completefunc validateEmail(email string) error { if len(email) > 254 { return errors.New("email too long") } if !emailRegex.MatchString(email) { return errors.New("invalid email format") } return nil}Static Analysis: Why It Matters
Security teams rely heavily on static analysis tools. Complex OOP code breaks these tools:
Metric | Complex OOP | Simple Code--------------------|------------|-------------Path coverage | 20-40% | 80-95%False positives | High | LowTaint tracking | Difficult | StraightforwardDependency scanning | Complex | Flat listWhen I write security code now, I ask: “Can a static analyzer verify this?” If the answer is no, I simplify.
Real-World Examples
OpenBSD’s Approach:
OpenBSD, known for security, follows:
- Code simplicity over features
- Auditable code over clever abstractions
- Fewer dependencies over more features
Go’s Standard Library:
// Simple API, hard to misusekey := sha256.Sum256([]byte("secret"))block, _ := aes.NewCipher(key[:])
// No inheritance, no factories, no DI// Just functions that do one thingRust’s Security Advantage:
// Compiler enforces single owner// No hidden shared state across class hierarchiesfn process_secure(data: Vec<u8>) -> Result<Processed, Error> { // data is consumed, cannot be used again // No side effects possible from other code Ok(Processed::from(data))}When Abstraction Is Actually Needed
I’m not saying abstraction is always bad. Abstraction is appropriate when:
- The complexity you’re hiding is truly irrelevant to security
- The abstraction reduces overall attack surface
- The abstraction is simple enough to fully audit
For example, a TLS library that hides certificate validation details is fine. The abstraction surface is well-defined and small. But a “base handler” with 10 subclasses where each might or might not validate? That’s a security problem.
Practical Guidelines
After years of writing and auditing security code, here’s what I follow:
1. Prefer functions over classes2. Prefer flat over hierarchical3. Prefer explicit over implicit4. Prefer composition over inheritance5. Prefer small dependencies over frameworks6. Prefer declarative over imperative7. Prefer pure functions over stateful objectsSummary
Security engineers prefer simple code because complexity is the enemy of security. Every abstraction layer creates potential hiding spots for bugs, vulnerabilities, and unexpected behavior.
The shift from annoyance with simplicity to appreciation is common. What initially seems limiting becomes essential. Fewer places for bugs to hide means faster reviews, more confident deployments, and ultimately, more secure systems.
For your next security-sensitive code, try this: start simple, add abstraction only when you can prove it reduces risk. Your future auditors (and attackers) will have less room to hide problems.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments