Secure Object Design
Learning Objectives
Validate at trust boundaries and enforce authorization in objects.
Prevent mass assignment, unsafe deserialization, insecure direct object references and data exposure.
Use defensive copies and avoid leaking mutable collections; handle secrets and resource limits.
Prerequisites: Modules 3, 18, 62.
Intuition and Definition
Security problems in OO code often come from objects trusting data they shouldn't, or exposing more than they should.
Validate at the boundary into value objects; reject, don't sanitize silently
Check permissions where the action happens (domain/service), not only in the UI
Bind requests to explicit DTOs with only allowed fields; never bind directly to entities Insecure direct object references
Check that the caller may access the object identified by an id
Data-only formats + schemas; never deserialize untrusted data into arbitrary types Sensitive data exposure
Separate DTOs; redact secrets in toString/logs Mutable collection leaks
Return read-only views or copies; copy mutable inputs on construction Secrets in objects
Short-lived, never logged, cleared after use where possible, kept in a secret store
Limits on sizes, counts, recursion depth, time Reflection/plugin boundaries Allow-lists, least privilege
Why It Matters
Most real-world vulnerabilities in business software (OWASP Top 10: broken access control, injection, insecure design, data exposure) are design flaws that objects can prevent by construction.
Syntax and Language Notes
public sealed record UpdateProfileRequest(string DisplayName); // only this field can be bound@dataclass(frozen=True)
class ApiKey:
value: str = field(repr=False) # never shown in repr/logsComplete Example
#include <algorithm>
#include <iostream>
#include <map>
#include <optional>
#include <span>
#include <stdexcept>
#include <string>
#include <vector>
class Secret { // never printed, never copied into logs
std::string value_;
public:
explicit Secret(std::string v) : value_(std::move(v)) {}
friend std::ostream& operator<<(std::ostream& os, const Secret&) { return os << "Secret(***)"; }
bool matches(const std::string& candidate) const { return candidate == value_; }
};
struct User { std::string id; bool isAdmin; };
class ServiceRecordBook {
std::map<std::string, std::string> ownerOfVin_; // vin -> owner user id
std::map<std::string, std::vector<std::string>> entries_;
public:
void registerVehicle(const std::string& vin, const std::string& owner) { ownerOfVin_[vin] = owner; }
// authorization inside the domain operation: no "trust the UI"
void addEntry(const User& u, const std::string& vin, const std::string& text) {
if (text.size() > 200) throw std::invalid_argument("entry too long"); // resource limit
if (!u.isAdmin) throw std::runtime_error("only workshop staff may add entries
…logging the key prints: Secret(***)
mallory add: only workshop staff may add entries
ana reads her vehicle: brake pads replaced
mallory reads V1: access denied
profile: Ana R., admin=0Step-by-Step Walkthrough
Secret overrides printing, so accidental logging shows Secret(***).
addEntry enforces *who* may write and a size limit, inside the domain object.
history checks that the caller owns the vehicle (or is staff) before returning data — knowing a VIN isn't enough.
history returns a std::span<const std::string>: callers can read but not modify the internal list.
Profile updates bind only the fields in UpdateProfileRequest; there's no way for a request to set isAdmin.
Common Mistakes
Authorization only in the UI or API gateway — any other entry point bypasses it.
Binding JSON directly to entities (isAdmin mass-assigned).
Returning internal mutable collections; storing caller-provided mutable objects without copying.
Logging whole objects that contain tokens, passwords or personal data.
Using exceptions' messages to leak details (stack traces, SQL) to clients.
Trade-offs and Alternatives
Security checks add code paths and tests; centralize policy (an AuthorizationService or policy objects) to avoid duplication, but call it from the domain operations. Defence in depth: validation at the edge *and* invariants in the domain.
Real-World Use
Banking and healthcare systems enforce object-level authorization in domain services; ASP.NET's [BindNever]/explicit DTOs and Rails' strong parameters prevent mass assignment; automotive diagnostic services require security access before write services (UDS 0x27/0x29).
Practice
Q1. (Easy) What is mass assignment?
Answer: Automatically binding request fields to an object's properties, letting attackers set fields they shouldn't (like isAdmin).
Q2. (Medium) What is an insecure direct object reference?
Answer: Accessing an object purely by an identifier from the request without checking that the caller is allowed to access that object.
Q3. (Advanced) Why make a defensive copy of a mutable list passed to a constructor?
Answer: The caller still holds a reference and could change the list later, bypassing the object's validation and invariants.
Interview Question
Q1. "Where should authorization checks live in an OO system?"
Answer: As close as possible to the protected operation — in the application/domain service or the entity method that performs it — using a central policy component for the rules, so every entry point (UI, API, batch jobs, message handlers) gets the same check. Edge checks (gateway, UI) are useful for early rejection and UX but never sufficient. Include object-level checks (does this user own this resource?), not just role checks.
Quick Quiz
Q1. Safe alternative to binary deserialization of untrusted input?
Answer: A data-only format (JSON, Protobuf) parsed into explicit DTOs with schema validation.
Q2. How do you stop secrets appearing in logs?
Answer: Wrap them in types whose string representation is redacted and never log raw values.
Q3. What should a getter return for an internal collection?
Answer: A read-only view or a copy.
Summary
Validate at boundaries, authorize in the domain, bind explicit DTOs, never trust identifiers alone.
Redact secrets, return read-only views, copy mutable inputs, limit resources.
Related: Modules 3, 62, 69.