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/logs

Complete 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=0

Step-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.