Security Checklist

Walk through a complete data lifecycle — from untrusted input to safe logging — and apply a concrete, practical security checklist at each stage, tying together Keychain, network security, SQLite, and input-validation practices covered earlier in this course.

Security is easiest to reason about as a pipeline: data enters your app as untrusted input, and at every subsequent stage it either gets safer (through validation, safe processing, and authorization) or it doesn't. This lesson organizes a practical, checklist-driven tour of Objective-C application security around exactly that pipeline, from the moment input arrives to the moment it is safely logged (or deliberately not logged at all). Rather than treating security as a separate, abstract topic, each stage below ties directly back to concrete techniques and earlier lessons in this course — Keychain, network security, SQLite, deep links, and JSON handling — so the checklist is grounded in code you already know how to write.

The security pipeline

Every piece of data your app handles — a network response, a user's typed input, a URL opened from another app, a value pulled out of a local database — passes conceptually through a pipeline before it becomes something you can trust and act on safely. Thinking in terms of that pipeline is more useful than memorizing an unordered checklist, because it tells you where a given defense belongs: validation happens at the boundary where untrusted data enters, authorization happens before a sensitive action executes, and safe logging is the very last stage, governing what ever gets written down at all.

The diagram below is the exact shape this lesson follows. Each stage gets its own section, and the checklist items from the specification are woven into the stage where they actually apply, so 'validate deep links' sits with input validation, 'use Keychain for secrets' sits with secure storage, and so on.

Input and validation: the first line of defense

Nothing that enters your app from outside its own process should be trusted until it has been validated: not a network response, not a value typed by the user, not a URL another app hands you to open. Validating untrusted input means checking that it is the shape, type, and range you expect before you act on it — a string that should be an email address should look like one, a number that represents an array index should be checked against bounds, a JSON payload should be checked for the fields your code actually needs before you unwrap them.

Deep links deserve specific attention here, and this is worth cross-referencing directly against the URL-scheme and deep-link coverage in the earlier App Security lesson: a URL scheme or universal link handler receives input that any other app (or a malicious website) can construct and hand to your app. Treating a deep link's parameters as trusted — for example, directly navigating to whatever account ID is embedded in the URL without checking that it belongs to the currently signed-in user — is a common and serious mistake. Every deep link handler should validate its scheme, host, path, and parameters explicitly before acting on them, and should never use a deep link alone to authorize a sensitive action (see the Authorization section below for why).

Safe processing: format strings

Once input is validated, how you process it still matters. Format-string vulnerabilities are one of the most Objective-C-specific security hazards on this checklist, because NSLog, +stringWithFormat:, and similar APIs interpret their first argument as a format string containing conversion specifiers like %@ and %d. If untrusted input is ever passed directly as that first argument rather than as a %@ substitution, an attacker who controls the input can inject their own format specifiers, which can crash the app (by referencing arguments that were never passed) or, in some circumstances, leak memory contents.

The fix is simple and absolute: untrusted input is always passed as an argument to a %@ (or other) specifier in a fixed, literal format string — never used as the format string itself. This should be a reflexive habit, not a case-by-case judgment call, because the vulnerable form and the safe form look almost identical at a glance and the difference is easy to introduce accidentally during a refactor.

Safe processing: buffer overflows in C code

Objective-C's Foundation layer (NSString, NSArray, and friends) manages memory and bounds safely, but Objective-C is a strict superset of C, and any C code you write or call into — manual buffer manipulation, C string functions, fixed-size arrays — is exposed to the same hazards covered in depth in the early C Arrays, C Pointers, and C Strings lessons. Writing past the end of a fixed-size buffer, using strcpy instead of a bounds-checked alternative, or trusting a length value that came from untrusted input without validating it against the actual buffer size are all classic buffer-overflow patterns that can corrupt memory or, in the worst case, be exploited for arbitrary code execution.

The bounds-checking discipline from those earlier C lessons is exactly the defense here: always know the actual capacity of a buffer, always validate a length or index against that capacity before writing, and prefer higher-level, bounds-safe APIs (NSMutableData, NSString) over raw C buffers whenever the performance-critical case does not specifically require them.

Safe processing: unsafe deserialization

Deserializing data — turning a JSON payload, a property list, or an archived object graph back into live Objective-C objects — is another point where untrusted input can do damage if handled carelessly. The earlier JSON and Serialization lesson's discussion of secure handling of untrusted data is directly relevant here: parsing untrusted JSON with NSJSONSerialization is generally safe because it produces plain Foundation collections, but unarchiving untrusted data with APIs that can instantiate arbitrary classes (older, non-secure-coding NSKeyedUnarchiver usage in particular) can be exploited to instantiate unexpected classes or trigger unintended code paths.

The practical defense is to prefer NSSecureCoding-conformant unarchiving (which requires you to specify the expected class or classes up front, and refuses to instantiate anything else) whenever you deserialize data that did not originate entirely within your own app's trusted storage, and to validate the shape of deserialized data (expected keys, expected types) before using it, exactly as you would validate any other untrusted input.

Authorization: checking before acting

Authorization is a distinct stage from validation, and it is easy to conflate the two. Validating input tells you the data is well-formed; authorization tells you whether the currently acting user or context is actually allowed to perform the requested action. Handling permissions correctly means checking authorization immediately before performing a sensitive action — deleting a record, transferring funds, revealing another user's data — not merely once at login, and not merely by having prompted the user for a system permission (like camera or location access) earlier in the session.

A common mistake is authorizing an action based on client-side state alone — for instance, trusting that a button was only shown to authorized users, so the action it triggers must be fine. Client-side UI gating is a convenience, not a security boundary; any sensitive action should be re-checked for authorization at the point where it actually executes, ideally with server-side enforcement backing up whatever the client checks, since a client can always be bypassed by a sufficiently motivated attacker.

Secure storage: credentials and the Keychain

Once data is validated, processed, and authorized, anything sensitive that needs to persist — passwords, tokens, API keys, cryptographic material — must be protected in storage, and the dedicated earlier Keychain lesson is the direct reference for how to do this correctly on Apple platforms. The Keychain provides encrypted, access-controlled storage specifically designed for exactly this class of secret, with options to require device unlock or biometric authentication before a secret can be read.

The checklist item 'protect credentials, use Keychain for secrets' is really a single rule: NSUserDefaults, a plist, or a file on disk in the app's sandbox are not appropriate places for a password, session token, or API key, because none of them provide the encryption-at-rest and access-control guarantees the Keychain does. Any code found writing a credential to NSUserDefaults should be treated as a security bug and migrated to Keychain storage.

Secure transport: HTTPS and SQL injection

Data in transit needs the same care as data at rest. The earlier Network Security lesson covers this in depth: every network request should use HTTPS, never plain HTTP, so that data cannot be read or tampered with in transit by anyone positioned on the network path. App Transport Security enforces this by default on Apple platforms, and any exception added to allow plain HTTP traffic should be treated as a deliberate, reviewed, and narrowly scoped decision, not a convenience default.

SQL injection belongs in this stage too, even though it is not strictly about network transport, because it is fundamentally about a trust boundary being crossed unsafely — untrusted input flowing into a query that is then executed with elevated trust, exactly like an unencrypted network request exposes data to an untrusted path. The earlier SQLite lesson covers the fix directly: always use prepared statements with bound parameters (sqlite3_bind_text and friends) rather than building SQL query strings by concatenating untrusted input directly into the query text. The vulnerable and safe patterns for this are shown side by side in the examples below.

Safe logging: the last stage

The final stage of the pipeline is logging, and it deserves its own discipline precisely because logs often live longer, and are visible to more people, than the request or session that produced them. The earlier Network Security lesson's warning about logging applies broadly: minimizing sensitive logging means never writing passwords, tokens, full credit card numbers, or other credentials to NSLog, os_log, or a crash-reporting service's breadcrumb trail, even 'temporarily' during debugging, because debug logging has a way of shipping to production and crash logs have a way of being uploaded to third-party services.

A practical habit is to redact or omit sensitive fields explicitly wherever a log statement touches a data structure that might contain them, rather than logging the whole object and trusting that no one will read the resulting log line closely. Treat every log statement as something that could eventually be read by someone other than the engineer who wrote it.

Closing the loop: dependencies, tooling, and testing

Three remaining checklist items apply across the whole pipeline rather than to a single stage, and are worth calling out as a dedicated closing discipline. First, update dependencies: third-party libraries and SDKs are part of your app's attack surface, and a known vulnerability in an outdated dependency is one of the most common ways real apps get compromised. Keeping dependencies current (and knowing what CVEs, if any, affect the versions you ship) is a security practice, not just a maintenance chore.

Second, use compiler and security tooling deliberately, not just for correctness. Address Sanitizer and the static analyzer, both discussed in earlier lessons primarily as correctness tools, are directly security-relevant too: a buffer overflow or use-after-free that Address Sanitizer catches in testing is exactly the class of bug that, left in production, could be exploited. Running these tools as part of routine testing, not just when chasing a specific crash, is a security practice.

Third, test security boundaries directly. A functional test suite that only exercises happy paths will never catch an authorization check that is missing on one code path, or an input validator that rejects the expected malformed cases but misses an edge case. Writing tests that specifically target security-sensitive code — attempting an unauthorized action and asserting it is rejected, feeding malformed or adversarial input to a parser and asserting it is handled safely — closes the loop between having a security checklist and actually verifying your code satisfies it.

Code Examples

The vulnerable version treats attacker-controlled text as the format string itself, so any % conversion specifiers embedded in that text are interpreted by NSLog or stringWithFormat:, which can crash the app or read unintended memory. The safe version uses a fixed, literal format string and passes the untrusted value only as a %@ argument, which neutralizes any conversion specifiers it might contain.

NSLog(@"Login attempt for username: %@", username);

Common mistake: Passing a variable directly as NSLog's or stringWithFormat:'s first argument whenever that variable might contain untrusted, attacker-influenced text.

The vulnerable version builds the SQL statement by string-formatting untrusted input directly into the query text, so an attacker who controls untrustedEmail can alter the query's logic entirely (classic SQL injection). The safe version, following the same prepared-statement approach as the earlier SQLite lesson, uses a placeholder (?) in the query and binds the untrusted value as data via sqlite3_bind_text, so it can never be interpreted as SQL syntax regardless of its contents.

sqlite3_bind_text(statement, 1, [untrustedEmail UTF8String], -1, SQLITE_TRANSIENT);

Common mistake: Building a SQL query string with stringWithFormat: (or any string concatenation) using untrusted input, instead of using a parameterized prepared statement.

NSUserDefaults persists to an unencrypted plist inside the app's sandbox; anyone with access to the device's filesystem (including, on a jailbroken device, other apps) can read it. The Keychain version, matching the pattern from the dedicated Keychain lesson, stores the same token encrypted, gated by kSecAttrAccessibleWhenUnlockedThisDeviceOnly so it is only readable while the device is unlocked and never migrates off this specific device via backup restore to a different device in a readable form. SecItemDelete before SecItemAdd is a common, defensive pattern to avoid errSecDuplicateItem when updating an existing credential.

// VULNERABLE: untrusted input used directly as the format string.
NSString *username = [self untrustedUsernameFromRequest];
NSLog(username); // If username contains "%@%@%@", this can crash or leak memory.

// SAFE: untrusted input passed only as an argument to a fixed format string.
NSLog(@"Login attempt for username: %@", username);

// Same rule applies to +stringWithFormat:
// VULNERABLE:
NSString *message = [NSString stringWithFormat:username];

// SAFE:
NSString *message = [NSString stringWithFormat:@"Message: %@", username];
// VULNERABLE: untrusted input concatenated directly into SQL text.
NSString *unsafeQuery = [NSString stringWithFormat:
    @"SELECT * FROM users WHERE email = '%@'", untrustedEmail];
sqlite3_exec(db, [unsafeQuery UTF8String], NULL, NULL, NULL);
// If untrustedEmail is: ' OR '1'='1
// the resulting query returns every row in the table.

// SAFE: a prepared statement with a bound parameter.
sqlite3_stmt *statement;
const char *sql = "SELECT * FROM users WHERE email = ?;";
if (sqlite3_prepare_v2(db, sql, -1, &statement, NULL) == SQLITE_OK) {
    sqlite3_bind_text(statement, 1, [untrustedEmail UTF8String], -1, SQLITE_TRANSIENT);
    while (sqlite3_step(statement) == SQLITE_ROW) {
        // Read the matching row(s) safely.
    }
    sqlite3_finalize(statement);
}
// VULNERABLE: storing a session token in NSUserDefaults (plaintext plist on disk).
[[NSUserDefaults standardUserDefaults] setObject:sessionToken forKey:@"authToken"];

// SAFE: storing the same token in the Keychain.
NSDictionary *query = @{
    (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
    (__bridge id)kSecAttrService: @"com.example.app.session",
    (__bridge id)kSecAttrAccount: userID,
    (__bridge id)kSecValueData: [sessionToken dataUsingEncoding:NSUTF8StringEncoding],
    (__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
};
SecItemDelete((__bridge CFDictionaryRef)query); // Clear any existing entry first.
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
if (status != errSecSuccess) {
    // Handle the failure — do not silently fall back to NSUserDefaults.
}

The application security pipeline

Every piece of data moves through these seven stages on its way through your app. Each stage in this lesson's sections corresponds to one step in this pipeline.

Input
 ↓
Validation
 ↓
Safe Processing
 ↓
Authorization
 ↓
Secure Storage
 ↓
Secure Transport
 ↓
Safe Logging

Important Rules

Validate untrusted input at the boundary, before it is acted on — including deep link parameters, which are attacker-controllable.

Never pass untrusted input directly as a format string; always pass it as a %@ argument to a fixed, literal format string.

In any C code, bounds-check buffer writes against actual capacity; prefer NSString/NSData over raw C buffers where performance allows.

Avoid SQL injection by always using prepared statements with bound parameters, never string-concatenated SQL.

Use HTTPS for all network traffic; treat any App Transport Security exception as a reviewed, narrowly scoped decision.

Protect credentials and secrets using the Keychain, never NSUserDefaults, a plist, or an unencrypted file.

Validate deep links (scheme, host, path, parameters) explicitly, and never use a deep link alone to authorize a sensitive action.

Avoid unsafe deserialization; prefer NSSecureCoding and validate the shape of deserialized data from untrusted sources.

Minimize sensitive logging; never log passwords, tokens, or other credentials, even temporarily during debugging.

Check authorization immediately before performing a sensitive action, not only once at login or after a permission prompt.

Keep third-party dependencies and SDKs updated to pick up security fixes.

Use Address Sanitizer and static analysis routinely, treating them as security tools, not just correctness tools.

Write tests that specifically target security-sensitive code paths, not only happy-path functional behavior.

Why the pipeline view matters in practice

Security bugs in real Objective-C apps rarely come from one dramatic mistake; they come from one stage of this pipeline being skipped while every other stage looks fine. A team might use HTTPS everywhere, store credentials in the Keychain, and use prepared statements for every query, and still ship a vulnerability because a single deep-link handler trusted an embedded parameter without validating it, or a debug NSLog statement logging a full request object accidentally included an auth token and shipped to production. Walking the pipeline deliberately — input, validation, safe processing, authorization, secure storage, secure transport, safe logging — during design and code review is what catches the one weak link before it ships, rather than relying on every engineer to remember an unordered list of unrelated rules.

NSLog(@"%@", userSuppliedString);

Using untrusted input directly as a format string lets it inject its own conversion specifiers, which can crash the app or expose memory contents. Always pass untrusted values as arguments to a fixed format string.

Building a SQL query with [NSString stringWithFormat:@"...%@...", untrustedValue] and executing it directly.

Use a prepared statement with a placeholder (?) and bind the untrusted value with sqlite3_bind_text.

Concatenating untrusted input into SQL text allows an attacker to alter the query's logic. Prepared statements bind values as data, never as executable SQL syntax.

Storing an API token or password in NSUserDefaults for 'quick access.'

Store credentials in the Keychain with an appropriate accessibility level.

NSUserDefaults is an unencrypted plist inside the app sandbox. The Keychain provides encryption at rest and access control specifically designed for secrets.

Trusting a deep link's embedded account ID or action without validating it belongs to the current user.

Validate every deep link's scheme, host, path, and parameters, and re-check authorization for any sensitive action it triggers.

A deep link can be constructed and sent by any other app or website. Treating its contents as trusted input is a direct path to unauthorized actions.

What is the correct fix for a format-string vulnerability like NSLog(userInput)? A. Wrap userInput in stringWithFormat: with no other arguments B. Pass userInput as a %@ argument to a fixed, literal format string, e.g. NSLog(@"%@", userInput) C. Remove NSLog calls entirely from the app D. Convert userInput to a C string first

B. Pass userInput as a %@ argument to a fixed, literal format string, e.g. NSLog(@"%@", userInput) — The vulnerability exists because untrusted text is interpreted as the format string itself. Passing it as an argument to a fixed format string neutralizes any embedded conversion specifiers.

Why are prepared statements the correct defense against SQL injection? A. They run faster than string-formatted queries B. They bind untrusted values as data rather than as part of the SQL syntax, so the values cannot alter the query's logic C. They automatically encrypt the database file D. They are the only way SQLite can execute a SELECT statement

B. They bind untrusted values as data rather than as part of the SQL syntax, so the values cannot alter the query's logic — A bound parameter is always treated as a literal value by the database engine, never as executable SQL, which prevents an attacker from injecting query logic through the input.

Where should credentials such as passwords and session tokens be stored on an Apple platform? A. NSUserDefaults, for fast access B. A plain text file in the app's Documents directory C. The Keychain, which provides encryption at rest and access control D. An in-memory static variable only

C. The Keychain, which provides encryption at rest and access control — The Keychain is specifically designed for secrets, providing encryption and configurable accessibility, unlike NSUserDefaults or plain files which are unencrypted within the app sandbox.

Why should deep link parameters never be trusted without validation? A. Deep links are deprecated on modern Apple platforms B. Deep links can be constructed and sent by any other app or website, making their contents attacker-controllable C. Deep links are automatically validated by the operating system D. Deep links cannot carry parameters

B. Deep links can be constructed and sent by any other app or website, making their contents attacker-controllable — Because any other app or a website can construct a deep link with arbitrary parameters, an app must validate scheme, host, path, and parameters explicitly rather than assuming they are trustworthy.

In the pipeline diagram from this lesson, which stage does 'minimize sensitive logging' belong to? A. Input B. Authorization C. Safe Logging (the final stage) D. Secure Transport

C. Safe Logging (the final stage) — Safe Logging is the last stage of the pipeline, governing what ever gets recorded, and is exactly where the checklist item about minimizing sensitive logging applies.

Besides catching bugs, why is Address Sanitizer described as a security-relevant tool in this lesson? A. Because it encrypts app data automatically B. Because memory-safety bugs like buffer overflows or use-after-free that it detects are exactly the class of bug that can be exploited if left in production C. Because it replaces the need for HTTPS D. Because it validates deep links automatically

B. Because memory-safety bugs like buffer overflows or use-after-free that it detects are exactly the class of bug that can be exploited if left in production — Address Sanitizer is usually framed as a correctness tool, but the memory-safety issues it catches (overflows, use-after-free) are precisely the kinds of bugs that pose real security risk if they reach production.

A login screen takes a username and password, sends them to a server over HTTPS, receives a session token, and stores it for future requests. It also logs the full server response 'for debugging.' Walk this flow through the seven-stage pipeline and identify what needs to change.

Input/Validation: the username and password should be validated for basic shape (non-empty, reasonable length) before sending. Safe Processing: no format-string or injection risk here since this is a network call, not raw NSLog of untrusted text or SQL. Authorization: not directly relevant to login itself, but any action taken immediately after login should re-check the token's validity, not just assume success. Secure Storage: the session token must be stored in the Keychain, not NSUserDefaults. Secure Transport: HTTPS is already correct. Safe Logging: logging the full server response is a serious problem if that response contains the session token or any other credential-like data; the debug log must redact or omit the token field specifically, and ideally not log the raw response at all outside of local debug builds.

Check each stage: is the username/password validated before sending? Is authorization checked anywhere? Where is the token stored? What does the debug log actually contain?