Loading learning content...
Security is often thought of as a system-level concern—firewalls, encryption, access control. But true security begins at the most fundamental level: how you design your classes and objects. A single poorly designed class can become the attack vector that undermines an otherwise secure system.
Secure object design is the practice of structuring classes to minimize vulnerability surface, prevent unintended state manipulation, protect sensitive data, and resist common attack patterns. It's the difference between an object that naively exposes its internals and one that maintains its integrity even under adversarial conditions.
This page examines the principles and patterns of secure object design—from immutability and defensive copying to encapsulation strategies and sensitive data handling. These techniques form the foundation of building software that doesn't just work, but works securely.
By the end of this page, you will understand how to design immutable objects that cannot be corrupted, implement defensive copying to protect internal state, handle sensitive data securely within objects, and apply encapsulation patterns that resist attack.
Secure object design rests on fundamental principles that guide class structure and behavior. These principles protect objects from misuse—whether accidental or malicious.
The Security Mindset:
Secure design requires thinking adversarially:
Every public interface is a contract, and in secure design, you must assume the other party might not honor it.
Identify where your code crosses trust boundaries—user input, external APIs, deserialization, inter-process communication. Objects at these boundaries require the most rigorous security practices.
Immutable objects are inherently thread-safe and resistant to corruption. Once created, their state cannot change, eliminating entire categories of bugs and vulnerabilities. Immutability is one of the most powerful tools in secure object design.
Why Immutability Enhances Security:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
// Insecure: Mutable object with exposed stateclass InsecureUserProfile { public name: string; public email: string; public permissions: string[]; // Mutable array exposed! constructor(name: string, email: string, permissions: string[]) { this.name = name; this.email = email; this.permissions = permissions; // Stores reference directly }} // Attack: Caller can modify internal stateconst perms = ['read'];const user = new InsecureUserProfile('Alice', 'alice@example.com', perms);perms.push('admin'); // User now has admin permissions!user.permissions.push('delete'); // Another way to corrupt state // Secure: Immutable object designclass SecureUserProfile { private readonly _name: string; private readonly _email: string; private readonly _permissions: ReadonlyArray<string>; private readonly _createdAt: Date; constructor( name: string, email: string, permissions: readonly string[] ) { // Validate inputs at construction if (!name || name.trim().length === 0) { throw new ValidationError('Name is required'); } if (!this.isValidEmail(email)) { throw new ValidationError('Invalid email format'); } // Store validated, immutable copies this._name = name.trim(); this._email = email.toLowerCase().trim(); this._permissions = Object.freeze([...permissions]); // Defensive copy + freeze this._createdAt = new Date(); // Freeze the object itself Object.freeze(this); } // Getters only - no setters get name(): string { return this._name; } get email(): string { return this._email; } get permissions(): ReadonlyArray<string> { return this._permissions; } get createdAt(): Date { return new Date(this._createdAt.getTime()); } // Defensive copy of mutable Date hasPermission(permission: string): boolean { return this._permissions.includes(permission); } // "Modifications" return new instances withEmail(newEmail: string): SecureUserProfile { return new SecureUserProfile(this._name, newEmail, this._permissions); } withAddedPermission(permission: string): SecureUserProfile { if (this._permissions.includes(permission)) { return this; // Already has permission, return same instance } return new SecureUserProfile( this._name, this._email, [...this._permissions, permission] ); } withRevokedPermission(permission: string): SecureUserProfile { return new SecureUserProfile( this._name, this._email, this._permissions.filter(p => p !== permission) ); } private isValidEmail(email: string): boolean { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); }} // Usage: State changes create new objectsconst user1 = new SecureUserProfile('Bob', 'bob@example.com', ['read']);const user2 = user1.withAddedPermission('write'); console.log(user1.permissions); // ['read'] - unchangedconsole.log(user2.permissions); // ['read', 'write'] - new objectImmutable Value Objects:
Value objects representing domain concepts (Money, Address, DateRange) should always be immutable:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
// Immutable Money value objectclass Money { private readonly _amount: bigint; // Use bigint for precision private readonly _currency: Currency; private constructor(amount: bigint, currency: Currency) { if (amount < 0n) { throw new InvalidMoneyError('Amount cannot be negative'); } this._amount = amount; this._currency = currency; Object.freeze(this); } static fromCents(cents: number, currency: Currency): Money { return new Money(BigInt(Math.round(cents)), currency); } static fromDecimal(decimal: number, currency: Currency): Money { const cents = BigInt(Math.round(decimal * Math.pow(10, currency.decimalPlaces))); return new Money(cents, currency); } static zero(currency: Currency): Money { return new Money(0n, currency); } get amount(): bigint { return this._amount; } get currency(): Currency { return this._currency; } toDecimal(): number { return Number(this._amount) / Math.pow(10, this._currency.decimalPlaces); } add(other: Money): Money { this.assertSameCurrency(other); return new Money(this._amount + other._amount, this._currency); } subtract(other: Money): Money { this.assertSameCurrency(other); const result = this._amount - other._amount; if (result < 0n) { throw new InsufficientFundsError('Subtraction would result in negative amount'); } return new Money(result, this._currency); } multiply(factor: number): Money { const result = BigInt(Math.round(Number(this._amount) * factor)); return new Money(result, this._currency); } isZero(): boolean { return this._amount === 0n; } isPositive(): boolean { return this._amount > 0n; } equals(other: Money): boolean { return this._amount === other._amount && this._currency.code === other._currency.code; } compareTo(other: Money): number { this.assertSameCurrency(other); if (this._amount < other._amount) return -1; if (this._amount > other._amount) return 1; return 0; } private assertSameCurrency(other: Money): void { if (this._currency.code !== other._currency.code) { throw new CurrencyMismatchError( `Cannot operate on ${this._currency.code} and ${other._currency.code}` ); } } toString(): string { return `${this._currency.symbol}${this.toDecimal().toFixed(this._currency.decimalPlaces)}`; }} // Usageconst price = Money.fromDecimal(29.99, Currency.USD);const tax = price.multiply(0.08);const total = price.add(tax);// price is unchanged, total is a new Money instanceSome objects must be mutable for performance or domain reasons. In these cases, apply defensive copying at boundaries, make internal state private, and use copy-on-write semantics where practical.
When immutability isn't possible, defensive copying protects objects from external modification. Defensive copying means making copies of mutable inputs when storing them and copies of mutable outputs when returning them.
Defensive Copy Rules:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
// Vulnerable to modification of internal stateclass VulnerableEvent { private participants: User[]; private schedule: Date[]; constructor(participants: User[], schedule: Date[]) { this.participants = participants; // Stores reference! this.schedule = schedule; } getParticipants(): User[] { return this.participants; // Returns internal reference! }} // Attack: const users = [alice];const dates = [new Date('2024-01-15')];const event = new VulnerableEvent(users, dates);users.push(mallory); // Mallory added without proper checks!event.getParticipants().push(eve); // Eve too!dates[0].setFullYear(1999); // Schedule corrupted! // Secure version with defensive copyingclass SecureEvent { private readonly _id: string; private readonly _name: string; private readonly _participants: Map<string, User>; private readonly _schedule: Date[]; private readonly _createdAt: Date; constructor( id: string, name: string, participants: readonly User[], schedule: readonly Date[] ) { // Validate if (!id || !name) { throw new ValidationError('Event requires id and name'); } if (participants.length === 0) { throw new ValidationError('Event requires at least one participant'); } this._id = id; this._name = name.trim(); this._createdAt = new Date(); // Defensive copy of participants (deep enough for our needs) this._participants = new Map( participants.map(p => [p.id, this.copyUser(p)]) ); // Defensive copy of dates (Date is mutable!) this._schedule = schedule.map(d => new Date(d.getTime())); // Validate schedule for (const date of this._schedule) { if (date < this._createdAt) { throw new ValidationError('Schedule cannot include past dates'); } } // Sort schedule for consistency this._schedule.sort((a, b) => a.getTime() - b.getTime()); } get id(): string { return this._id; } get name(): string { return this._name; } get createdAt(): Date { return new Date(this._createdAt.getTime()); } // Defensive copy // Return defensive copies getParticipants(): User[] { return Array.from(this._participants.values()).map(u => this.copyUser(u)); } getSchedule(): Date[] { return this._schedule.map(d => new Date(d.getTime())); } getParticipantById(id: string): User | undefined { const user = this._participants.get(id); return user ? this.copyUser(user) : undefined; } // Modifications return new instances (immutable style) addParticipant(user: User): SecureEvent { if (this._participants.has(user.id)) { throw new DuplicateParticipantError(user.id); } return new SecureEvent( this._id, this._name, [...this.getParticipants(), user], this.getSchedule() ); } removeParticipant(userId: string): SecureEvent { if (!this._participants.has(userId)) { throw new ParticipantNotFoundError(userId); } if (this._participants.size === 1) { throw new ValidationError('Cannot remove last participant'); } const remaining = this.getParticipants().filter(p => p.id !== userId); return new SecureEvent(this._id, this._name, remaining, this.getSchedule()); } private copyUser(user: User): User { // Create a defensive copy of User // In practice, if User is immutable, just return it return { id: user.id, name: user.name, email: user.email, // Deep copy any mutable properties... }; }} // Now attacks fail:const users2 = [alice];const dates2 = [new Date('2024-06-15')];const secureEvent = new SecureEvent('evt-1', 'Meeting', users2, dates2); users2.push(mallory); // No effect - internal copy was madedates2[0].setFullYear(1999); // No effect - dates were copied const retrieved = secureEvent.getParticipants();retrieved.push(eve); // No effect - returned a copyconsole.log(secureEvent.getParticipants().length); // Still 1Defensive copying has performance costs. Copy only as deep as necessary for security. If User is an immutable object, you don't need to copy it. If User has mutable fields you care about, copy those. Profile and measure if performance is a concern.
Encapsulation—hiding implementation details behind a controlled interface—is fundamental to secure object design. Proper encapsulation limits the ways an object can be interacted with, reducing attack surface.
Encapsulation Security Strategies:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
// Pattern 1: Private state with controlled accessclass SecureBankAccount { private readonly _accountId: string; private _balance: number; private _locked: boolean = false; private readonly _transactionHistory: Transaction[] = []; private readonly _maxDailyWithdrawal: number = 10000; constructor(accountId: string, initialBalance: number) { if (initialBalance < 0) { throw new InvalidAmountError('Initial balance cannot be negative'); } this._accountId = accountId; this._balance = initialBalance; Object.seal(this); // Prevent adding new properties } // Public read-only access to balance get balance(): number { return this._balance; } get isLocked(): boolean { return this._locked; } // Controlled modification through validated methods deposit(amount: number): void { this.ensureAccountActive(); this.validatePositiveAmount(amount); this._balance += amount; this.recordTransaction('DEPOSIT', amount); } withdraw(amount: number): void { this.ensureAccountActive(); this.validatePositiveAmount(amount); this.ensureSufficientFunds(amount); this.ensureWithinDailyLimit(amount); this._balance -= amount; this.recordTransaction('WITHDRAWAL', amount); } // Administrative operations require authorization lock(authorizedBy: string, reason: string): void { if (!authorizedBy) { throw new UnauthorizedOperationError('Lock requires authorization'); } this._locked = true; this.recordTransaction('LOCK', 0, { authorizedBy, reason }); } unlock(authorizedBy: string): void { if (!authorizedBy) { throw new UnauthorizedOperationError('Unlock requires authorization'); } this._locked = false; this.recordTransaction('UNLOCK', 0, { authorizedBy }); } // Return copy of transaction history getTransactionHistory(): ReadonlyArray<Transaction> { return [...this._transactionHistory]; } // Private validation methods private ensureAccountActive(): void { if (this._locked) { throw new AccountLockedError('Account is locked'); } } private validatePositiveAmount(amount: number): void { if (amount <= 0 || !Number.isFinite(amount)) { throw new InvalidAmountError('Amount must be positive finite number'); } } private ensureSufficientFunds(amount: number): void { if (amount > this._balance) { throw new InsufficientFundsError(`Cannot withdraw ${amount}, balance is ${this._balance}`); } } private ensureWithinDailyLimit(amount: number): void { const today = new Date(); today.setHours(0, 0, 0, 0); const todayWithdrawals = this._transactionHistory .filter(t => t.type === 'WITHDRAWAL' && t.timestamp >= today) .reduce((sum, t) => sum + t.amount, 0); if (todayWithdrawals + amount > this._maxDailyWithdrawal) { throw new DailyLimitExceededError(`Would exceed daily limit of ${this._maxDailyWithdrawal}`); } } private recordTransaction(type: string, amount: number, metadata?: object): void { this._transactionHistory.push({ type, amount, timestamp: new Date(), balanceAfter: this._balance, metadata, }); }} // Pattern 2: Factory methods to control instantiationclass SecureSession { private static readonly sessions = new Map<string, SecureSession>(); private readonly _id: string; private readonly _userId: string; private readonly _createdAt: Date; private _expiresAt: Date; private _lastActivityAt: Date; private _isRevoked: boolean = false; // Private constructor - cannot be called directly private constructor(id: string, userId: string, durationMs: number) { this._id = id; this._userId = userId; this._createdAt = new Date(); this._lastActivityAt = new Date(); this._expiresAt = new Date(Date.now() + durationMs); } // Factory method controls creation static create(userId: string, durationMs: number = 3600000): SecureSession { const id = crypto.randomUUID(); const session = new SecureSession(id, userId, durationMs); SecureSession.sessions.set(id, session); return session; } // Lookup method with validation static getById(id: string): SecureSession | null { const session = SecureSession.sessions.get(id); if (!session) return null; if (session.isExpired() || session._isRevoked) { SecureSession.sessions.delete(id); return null; } return session; } get id(): string { return this._id; } get userId(): string { return this._userId; } isExpired(): boolean { return new Date() > this._expiresAt; } isValid(): boolean { return !this._isRevoked && !this.isExpired(); } touch(): void { if (!this.isValid()) { throw new SessionInvalidError('Cannot touch invalid session'); } this._lastActivityAt = new Date(); } extend(additionalMs: number): void { if (!this.isValid()) { throw new SessionInvalidError('Cannot extend invalid session'); } this._expiresAt = new Date(this._expiresAt.getTime() + additionalMs); } revoke(): void { this._isRevoked = true; SecureSession.sessions.delete(this._id); }}Object.freeze() makes an object completely immutable—no property changes or additions. Object.seal() allows property changes but prevents additions/deletions. Use freeze for immutable value objects, seal for objects with controlled mutability.
Objects that handle sensitive data (passwords, tokens, personal information, financial data) require special care. Sensitive data must be protected throughout its lifecycle: in transit, at rest, and especially in memory.
Sensitive Data Principles:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
// Secure wrapper for sensitive stringsclass SecureString { private _value: string | null; private _cleared: boolean = false; constructor(value: string) { this._value = value; } // Controlled access to the value use<T>(consumer: (value: string) => T): T { if (this._cleared || this._value === null) { throw new SecureStringClearedError('Value has been cleared'); } return consumer(this._value); } // Explicit clearing - call when done with the value clear(): void { // In languages with direct memory access, you would zero the memory // In JS/TS, we can only null the reference this._value = null; this._cleared = true; } // Prevent accidental exposure toString(): string { return '[REDACTED]'; } toJSON(): string { return '[REDACTED]'; } // Utility for comparing without exposing equals(other: SecureString): boolean { if (this._cleared || other._cleared) return false; return this.timingSafeEquals(this._value!, other._value!); } private timingSafeEquals(a: string, b: string): boolean { if (a.length !== b.length) return false; let result = 0; for (let i = 0; i < a.length; i++) { result |= a.charCodeAt(i) ^ b.charCodeAt(i); } return result === 0; }} // Secure password handlingclass Password { private readonly _hash: string; // Private constructor - use factory methods private constructor(hash: string) { this._hash = hash; } // Create from plaintext - hashes immediately static async fromPlaintext(plaintext: SecureString): Promise<Password> { let hash: string; try { hash = await plaintext.use(async (pt) => { // Use proper password hashing (bcrypt, Argon2) return await bcrypt.hash(pt, 12); }); } finally { plaintext.clear(); // Clear plaintext immediately } return new Password(hash); } // Create from stored hash (e.g., from database) static fromHash(hash: string): Password { return new Password(hash); } // Verify a password attempt async verify(attempt: SecureString): Promise<boolean> { try { return await attempt.use(async (pt) => { return await bcrypt.compare(pt, this._hash); }); } finally { attempt.clear(); } } // Get hash for storage getHashForStorage(): string { return this._hash; } // Prevent exposure toString(): string { return '[PASSWORD]'; } toJSON(): never { throw new Error('Password cannot be serialized'); }} // Credit card with maskingclass CreditCard { private readonly _number: string; private readonly _expiry: string; private readonly _cvv: string; private readonly _last4: string; private readonly _cardType: CardType; constructor(number: string, expiry: string, cvv: string) { // Validate const cleanNumber = number.replace(/\s/g, ''); if (!this.isValidLuhn(cleanNumber)) { throw new InvalidCardNumberError('Invalid card number'); } if (!this.isValidExpiry(expiry)) { throw new InvalidExpiryError('Invalid expiry date'); } if (!/^\d{3,4}$/.test(cvv)) { throw new InvalidCvvError('Invalid CVV'); } this._number = cleanNumber; this._expiry = expiry; this._cvv = cvv; this._last4 = cleanNumber.slice(-4); this._cardType = this.detectCardType(cleanNumber); Object.freeze(this); } // Only expose masked/safe data get last4(): string { return this._last4; } get cardType(): CardType { return this._cardType; } get maskedNumber(): string { return `****-****-****-${this._last4}`; } // For payment processing only - controlled access useForPayment<T>(processor: (number: string, expiry: string, cvv: string) => T): T { return processor(this._number, this._expiry, this._cvv); } // Secure tokenization async tokenize(tokenizer: CardTokenizer): Promise<string> { return this.useForPayment((number, expiry, cvv) => { return tokenizer.tokenize(number, expiry, cvv); }); } // Prevent any exposure toString(): string { return `${this._cardType} ending in ${this._last4}`; } toJSON(): object { return { type: this._cardType, last4: this._last4, // Never include full number, expiry, or CVV }; } private isValidLuhn(number: string): boolean { let sum = 0; let isEven = false; for (let i = number.length - 1; i >= 0; i--) { let digit = parseInt(number[i], 10); if (isEven) { digit *= 2; if (digit > 9) digit -= 9; } sum += digit; isEven = !isEven; } return sum % 10 === 0; } private isValidExpiry(expiry: string): boolean { const match = expiry.match(/^(\d{2})\/(\d{2})$/); if (!match) return false; const month = parseInt(match[1], 10); const year = parseInt('20' + match[2], 10); if (month < 1 || month > 12) return false; const now = new Date(); const cardDate = new Date(year, month); return cardDate > now; } private detectCardType(number: string): CardType { if (/^4/.test(number)) return CardType.VISA; if (/^5[1-5]/.test(number)) return CardType.MASTERCARD; if (/^3[47]/.test(number)) return CardType.AMEX; return CardType.UNKNOWN; }}In managed languages (Java, C#, JavaScript), you cannot securely wipe memory. Strings are immutable and GC timing is unpredictable. Minimize sensitive data lifetime, avoid unnecessary copies, and consider using byte arrays that can be zeroed where language permits.
Serialization—converting objects to data formats like JSON or binary—introduces significant security risks. Improper serialization can expose sensitive data, enable injection attacks, or allow arbitrary object creation.
Serialization Security Risks:
| Vulnerability | Description | Mitigation |
|---|---|---|
| Sensitive Data Exposure | Private fields included in output | Explicit allowlists, exclude sensitive fields |
| Deserialization of Untrusted Data | Arbitrary object creation from input | Validate types, use DTOs, avoid direct deserialization |
| Object Injection | Crafted input creates dangerous objects | Type validation, sealed hierarchies |
| Prototype Pollution | Input modifies Object prototype (JS) | Freeze prototypes, validate keys |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
// Pattern 1: Explicit serialization methodsclass User { private readonly _id: string; private readonly _email: string; private readonly _passwordHash: string; // NEVER serialize this private readonly _socialSecurityNumber: string; // Or this private readonly _roles: string[]; constructor(id: string, email: string, passwordHash: string, ssn: string, roles: string[]) { this._id = id; this._email = email; this._passwordHash = passwordHash; this._socialSecurityNumber = ssn; this._roles = [...roles]; } // Public API representation - safe to expose toPublicDto(): UserPublicDto { return { id: this._id, email: this._email, }; } // Admin representation - more detail, still secure toAdminDto(): UserAdminDto { return { id: this._id, email: this._email, roles: [...this._roles], hasPassword: !!this._passwordHash, ssnLast4: this._socialSecurityNumber.slice(-4), }; } // Internal representation for storage toStorageDto(): UserStorageDto { return { id: this._id, email: this._email, passwordHash: this._passwordHash, // OK for secure storage ssn: this._socialSecurityNumber, // Must encrypt at rest roles: this._roles, }; } // Block default serialization toJSON(): object { return this.toPublicDto(); // Default to safest representation }} // Pattern 2: Safe deserialization with DTOsinterface CreateUserRequest { email: string; password: string; name: string;} class UserFactory { constructor( private readonly passwordHasher: PasswordHasher, private readonly emailValidator: EmailValidator, private readonly userRepository: UserRepository ) {} async createFromRequest(request: unknown): Promise<User> { // Step 1: Validate structure const dto = this.validateAndParseRequest(request); // Step 2: Validate business rules await this.validateEmail(dto.email); await this.validatePassword(dto.password); await this.ensureEmailNotTaken(dto.email); // Step 3: Create secure objects const passwordHash = await this.passwordHasher.hash(dto.password); const user = new User( crypto.randomUUID(), dto.email.toLowerCase().trim(), passwordHash, dto.name.trim() ); return user; } private validateAndParseRequest(request: unknown): CreateUserRequest { if (!request || typeof request !== 'object') { throw new ValidationError('Invalid request format'); } const obj = request as Record<string, unknown>; // Explicit field extraction - ignores unexpected fields const email = this.extractString(obj, 'email'); const password = this.extractString(obj, 'password'); const name = this.extractString(obj, 'name'); return { email, password, name }; } private extractString(obj: Record<string, unknown>, key: string): string { const value = obj[key]; if (typeof value !== 'string') { throw new ValidationError(`${key} must be a string`); } return value; } private async validateEmail(email: string): Promise<void> { if (!this.emailValidator.isValid(email)) { throw new ValidationError('Invalid email format'); } } private async validatePassword(password: string): Promise<void> { if (password.length < 12) { throw new ValidationError('Password must be at least 12 characters'); } // Additional strength checks... } private async ensureEmailNotTaken(email: string): Promise<void> { const existing = await this.userRepository.findByEmail(email); if (existing) { throw new ConflictError('Email already in use'); } }} // Pattern 3: Preventing prototype pollution (JavaScript-specific)function safeJsonParse<T>(json: string, validator: (obj: unknown) => obj is T): T { const parsed = JSON.parse(json, (key, value) => { // Reject dangerous keys if (key === '__proto__' || key === 'constructor' || key === 'prototype') { throw new SecurityError('Dangerous key in JSON'); } return value; }); if (!validator(parsed)) { throw new ValidationError('Invalid JSON structure'); } return parsed;} // Usage with type guardinterface SafeRequest { action: string; data: Record<string, string>;} function isSafeRequest(obj: unknown): obj is SafeRequest { if (!obj || typeof obj !== 'object') return false; const o = obj as Record<string, unknown>; return typeof o.action === 'string' && typeof o.data === 'object' && o.data !== null;} const request = safeJsonParse(untrustedJson, isSafeRequest);Always deserialize to DTOs (Data Transfer Objects) with explicit validation, then use factories to create domain objects. This prevents attackers from setting internal state by crafting JSON with private field names.
Race conditions in concurrent code can create security vulnerabilities. TOCTOU (Time-of-Check-to-Time-of-Use) vulnerabilities occur when an adversary modifies state between a security check and the operation that depends on it.
Thread Safety Security Patterns:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
// TOCTOU vulnerability exampleclass VulnerableAccountService { async transfer(fromId: string, toId: string, amount: number): Promise<void> { const fromAccount = await this.getAccount(fromId); // Check balance if (fromAccount.balance < amount) { throw new InsufficientFundsError(); } // VULNERABILITY: Balance could change between check and update! // Another thread could withdraw funds in this window await this.updateBalance(fromId, fromAccount.balance - amount); await this.updateBalance(toId, amount); // Could also fail, leaving inconsistent state }} // Secure version with atomic operationsclass SecureAccountService { constructor( private readonly accountRepository: AccountRepository, private readonly lockManager: DistributedLockManager ) {} async transfer( fromId: string, toId: string, amount: number, idempotencyKey: string ): Promise<TransferResult> { // Ensure consistent lock order to prevent deadlocks const [firstId, secondId] = [fromId, toId].sort(); // Acquire locks atomically const locks = await this.lockManager.acquireMultiple( [`account:${firstId}`, `account:${secondId}`], { timeout: 5000 } ); try { // Check for duplicate (idempotency) const existing = await this.findTransferByKey(idempotencyKey); if (existing) { return existing.result; } // Atomic check-and-update within transaction return await this.accountRepository.transaction(async (tx) => { const fromAccount = await tx.findById(fromId, { forUpdate: true }); const toAccount = await tx.findById(toId, { forUpdate: true }); if (!fromAccount || !toAccount) { throw new AccountNotFoundError(); } if (fromAccount.isFrozen || toAccount.isFrozen) { throw new AccountFrozenError(); } // Check within transaction - no TOCTOU possible if (fromAccount.balance < amount) { throw new InsufficientFundsError(); } // Atomic updates await tx.updateBalance(fromId, fromAccount.balance - amount); await tx.updateBalance(toId, toAccount.balance + amount); // Record transfer const transfer = await tx.createTransfer({ fromAccountId: fromId, toAccountId: toId, amount, idempotencyKey, timestamp: new Date(), }); return { success: true, transferId: transfer.id }; }); } finally { await locks.release(); } }} // Immutable approach - avoids race conditions entirelyclass ImmutableBalance { private readonly _amount: bigint; private readonly _version: number; constructor(amount: bigint, version: number) { this._amount = amount; this._version = version; Object.freeze(this); } get amount(): bigint { return this._amount; } get version(): number { return this._version; } debit(amount: bigint): ImmutableBalance { if (amount > this._amount) { throw new InsufficientFundsError(); } return new ImmutableBalance(this._amount - amount, this._version + 1); } credit(amount: bigint): ImmutableBalance { return new ImmutableBalance(this._amount + amount, this._version + 1); }} // Optimistic locking with version checkclass OptimisticAccountRepository { async updateBalance( accountId: string, newBalance: ImmutableBalance, expectedVersion: number ): Promise<void> { const updated = await this.db.query(` UPDATE accounts SET balance = $1, version = $2 WHERE id = $3 AND version = $4 `, [newBalance.amount, newBalance.version, accountId, expectedVersion]); if (updated.rowCount === 0) { throw new OptimisticLockException( 'Account was modified by another transaction' ); } }}The best defense against TOCTOU is immutability. If objects cannot change, there's no window for modification. Use immutable value objects, atomic operations, and optimistic locking to eliminate race condition vulnerabilities.
Secure object design creates classes that protect themselves from misuse and attack. Let's consolidate the key principles:
What's next:
With secure object design principles established, the next page explores input sanitization—the techniques for cleaning and validating external input before it enters your secure objects.
You now understand how to design objects that are inherently secure. Apply immutability, defensive copying, and careful encapsulation to create classes that resist attack and maintain integrity even under adversarial conditions.