Cookie Poisoning
ID |
swift.cookie_poisoning |
Severity |
high |
Resource |
Injection |
Language |
Swift |
Tags |
CWE:472, NIST.SP.800-53, OWASP:2021:A04, PCI-DSS:6.5.1 |
Description
Improper neutralization of external input stored into a cookie ('Cookie Poisoning').
In iOS and server-side Swift applications, cookies are used for session management, user preferences, and authentication. When untrusted user input flows directly into cookie values without proper sanitization, attackers can manipulate cookie properties or inject malicious data that affects application behavior.
Rationale
If an attacker inserts a malicious payload into a browser cookie, the application might mistakenly accept it as legitimate.
This compromised cookie could then bypass security measures, alter user settings, or execute unexpected actions under the logged-in user’s identity.
Furthermore, since cookies are usually specified in the 'Set-Cookie' HTTP header within the response message, an attacker could also initiate a header manipulation attack by including CR/LF characters.
Depending on how the cookie is used, the attacker could compromise authentication, session management, or leak sensitive information.
Consider these examples demonstrating vulnerable cookie handling in Swift:
Foundation HTTPCookie (iOS/macOS)
import Foundation
class CookieHandler {
// VULNERABLE: User input flows into cookie without sanitization
func setUserPreference_vulnerable1(request: URLRequest) {
// User-controlled input from query parameter
guard let url = request.url,
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems,
let themeName = queryItems.first(where: { $0.name == "theme" })?.value else {
return
}
// FLAW: Unsanitized user input goes directly into cookie value
let properties: [HTTPCookiePropertyKey: Any] = [
.name: "user_theme",
.value: themeName, // Tainted data from user input
.domain: "example.com",
.path: "/"
]
if let cookie = HTTPCookie(properties: properties) {
HTTPCookieStorage.shared.setCookie(cookie)
}
}
// VULNERABLE: Cookie value from user-controlled header
func setSessionData_vulnerable2(headers: [String: String]) {
// FLAW: User can inject malicious data via X-Session-Data header
if let sessionData = headers["X-Session-Data"] {
let properties: [HTTPCookiePropertyKey: Any] = [
.name: "session_data",
.value: sessionData, // Unsanitized user input
.domain: "example.com",
.path: "/"
]
if let cookie = HTTPCookie(properties: properties) {
HTTPCookieStorage.shared.setCookie(cookie)
}
}
}
// VULNERABLE: Cookie name from user input
func setDynamicCookie_vulnerable3(cookieName: String, value: String) {
// FLAW: Both cookie name and value are user-controlled
let properties: [HTTPCookiePropertyKey: Any] = [
.name: cookieName, // User-controlled cookie name
.value: value, // User-controlled cookie value
.domain: "example.com",
.path: "/"
]
if let cookie = HTTPCookie(properties: properties) {
HTTPCookieStorage.shared.setCookie(cookie)
}
}
}
Vapor Framework (Server-Side Swift)
import Vapor
class VaporCookieHandler {
// VULNERABLE: User input from request parameters flows into cookie
func setUserCookie_vulnerable4(req: Request) throws -> Response {
// FLAW: Unsanitized query parameter used as cookie value
guard let userData = req.query[String.self, at: "user_data"] else {
throw Abort(.badRequest)
}
let cookie = HTTPCookies.Value(
name: "userData",
value: userData // Tainted data from query string
)
var response = Response(status: .ok)
response.cookies[cookie.name] = cookie
return response
}
// VULNERABLE: Cookie value from request body
func setPreferences_vulnerable5(req: Request) throws -> Response {
// FLAW: Unsanitized JSON data used as cookie value
let preferences = try req.content.decode(PreferencesData.self)
let cookie = HTTPCookies.Value(
name: "preferences",
value: preferences.rawValue // User-controlled data
)
var response = Response(status: .ok)
response.cookies["preferences"] = cookie
return response
}
// VULNERABLE: Cookie value concatenated with user input
func setTrackingCookie_vulnerable6(req: Request) throws -> Response {
guard let userId = req.query[String.self, at: "uid"] else {
throw Abort(.badRequest)
}
// FLAW: User input concatenated into cookie value without sanitization
let trackingValue = "user_\(userId)_session"
let cookie = HTTPCookies.Value(
name: "tracking",
value: trackingValue // Contains unsanitized user input
)
var response = Response(status: .ok)
response.cookies["tracking"] = cookie
return response
}
}
struct PreferencesData: Content {
var rawValue: String
}
In these vulnerable examples:
-
Direct query parameter flow: User-controlled query parameters flow directly into cookie values without validation or encoding.
-
Header injection: HTTP headers controlled by users are used as cookie values, allowing header manipulation attacks.
-
Cookie name poisoning: User input determines the cookie name, potentially overwriting critical cookies or injecting special characters.
-
JSON data flow: User-submitted JSON data flows into cookies without sanitization.
-
String concatenation: User input is concatenated into cookie values, allowing injection of special characters (
;,=, CR/LF).
Attackers could exploit these vulnerabilities by:
- Injecting CR/LF characters to manipulate cookie properties (e.g., value; Path=/admin; Secure)
- Overwriting sensitive cookies by controlling cookie names
- Injecting malicious data that affects application logic when cookies are read
- Performing session fixation or hijacking attacks
Remediation
Ensure all user inputs are validated and sanitized before inclusion in cookies.
A strict whitelisting pattern could be used to prevent unintended modification of the cookie properties. A blacklist pattern that blocks dangerous characters(;, ,, =, CR/LR, etc.) in the user-controlled input to use in the cookie value is also possible, but a whitelisting pattern is recommended.
It is common to base64-encode the cookie value before storing it in the cookie: the given value cannot alter the cookie properties (Max-Age / Expires, Domain, Path, Secure, HttpOnly, SameSite). This approach also avoids the cookie poisoning issue, when any value is allowed for the cookie value.
An alternative to cookies is server-side session storage, if possible. This way you eliminate cookie poisoning risk completely.
For session cookies, never make them dependent on external input. The session identifiers must be unique and randomly generated so that they are not predictable, and unusable after the session is closed. If possible, do not reinvent the wheel and implement your own session handling, as mature session libraries and frameworks exist.
Another important design principle is to use each cookie for only one task. It can be tempting to use multi-purpose cookies, but this creates security risks. Never mix session identifiers, anti-CSRF tokens, password reset tokens or 'remember-me' tokens in the same cookie.
Cookie properties should have appropriate values, such as HttpOnly and Secure flags, in addition to avoiding cookie poisoning. The unsafe_cookie detector can be used to detect misconfigurations in cookie properties.
Here are secure implementations for Swift:
Foundation HTTPCookie (Secure)
import Foundation
import CryptoKit
class SecureCookieHandler {
// SECURE: Whitelist validation for cookie values
func setUserPreference_secure1(request: URLRequest) {
guard let url = request.url,
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems,
let themeName = queryItems.first(where: { $0.name == "theme" })?.value else {
return
}
// SECURE: Whitelist only allowed theme values
let allowedThemes = ["light", "dark", "auto"]
guard allowedThemes.contains(themeName) else {
print("Invalid theme name rejected")
return
}
let properties: [HTTPCookiePropertyKey: Any] = [
.name: "user_theme",
.value: themeName, // Validated against whitelist
.domain: "example.com",
.path: "/"
]
if let cookie = HTTPCookie(properties: properties) {
HTTPCookieStorage.shared.setCookie(cookie)
}
}
// SECURE: Base64 encoding for arbitrary cookie values
func setSessionData_secure2(headers: [String: String]) {
if let sessionData = headers["X-Session-Data"] {
// SECURE: Base64 encode the value to prevent injection
let encodedData = Data(sessionData.utf8).base64EncodedString()
let properties: [HTTPCookiePropertyKey: Any] = [
.name: "session_data",
.value: encodedData, // Base64-encoded, safe from injection
.domain: "example.com",
.path: "/"
]
if let cookie = HTTPCookie(properties: properties) {
HTTPCookieStorage.shared.setCookie(cookie)
}
}
}
// SECURE: Use server-generated identifiers instead of user input
func setSessionIdentifier_secure3(userId: String) {
// SECURE: Generate cryptographically random session ID
var bytes = [UInt8](repeating: 0, count: 32)
_ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
let sessionId = Data(bytes).base64EncodedString()
// Store mapping on server: sessionId -> userId
// Don't put userId directly in cookie
let properties: [HTTPCookiePropertyKey: Any] = [
.name: "session_id",
.value: sessionId, // Random, not user-controlled
.domain: "example.com",
.path: "/",
.secure: true,
.init(rawValue: "HttpOnly"): true
]
if let cookie = HTTPCookie(properties: properties) {
HTTPCookieStorage.shared.setCookie(cookie)
}
}
}
Vapor Framework (Secure)
import Vapor
import Foundation
class SecureVaporCookieHandler {
// SECURE: Whitelist validation for user preferences
func setUserCookie_secure4(req: Request) throws -> Response {
guard let userData = req.query[String.self, at: "user_data"] else {
throw Abort(.badRequest)
}
// SECURE: Strict pattern validation
let alphanumericPattern = "^[a-zA-Z0-9_-]{1,50}$"
let regex = try NSRegularExpression(pattern: alphanumericPattern)
let range = NSRange(userData.startIndex..., in: userData)
guard regex.firstMatch(in: userData, range: range) != nil else {
throw Abort(.badRequest, reason: "Invalid user data format")
}
let cookie = HTTPCookies.Value(
name: "userData",
value: userData, // Validated against strict pattern
isSecure: true,
isHTTPOnly: true
)
var response = Response(status: .ok)
response.cookies[cookie.name] = cookie
return response
}
// SECURE: Base64 encoding for arbitrary data
func setPreferences_secure5(req: Request) throws -> Response {
let preferences = try req.content.decode(PreferencesData.self)
// SECURE: Base64 encode to prevent cookie injection
let encodedValue = Data(preferences.rawValue.utf8).base64EncodedString()
let cookie = HTTPCookies.Value(
name: "preferences",
value: encodedValue, // Base64-encoded, safe
isSecure: true,
isHTTPOnly: true
)
var response = Response(status: .ok)
response.cookies["preferences"] = cookie
return response
}
// SECURE: Server-side session storage instead of cookies
func createSession_secure6(req: Request) throws -> Response {
guard let userId = req.query[String.self, at: "uid"] else {
throw Abort(.badRequest)
}
// SECURE: Generate random session ID, store userId on server
let sessionId = UUID().uuidString
// Store in server-side session storage
req.session.data["userId"] = userId
req.session.data["createdAt"] = "\(Date())"
// Cookie only contains random session ID
let cookie = HTTPCookies.Value(
name: "session_id",
value: sessionId, // Random, not user-controlled
isSecure: true,
isHTTPOnly: true,
sameSite: .strict
)
var response = Response(status: .ok)
response.cookies["session_id"] = cookie
return response
}
// SECURE: Character blacklist for simple validation
func sanitizeCookieValue(value: String) -> String? {
// Block dangerous characters: ; , = CR LF
let dangerousChars = CharacterSet(charactersIn: ";,=\r\n")
guard value.rangeOfCharacter(from: dangerousChars) == nil else {
return nil
}
return value
}
}
Best Practices for Swift Cookie Security:
-
Whitelist validation: Only allow known-good values from a predefined set:
swift let allowedValues = ["option1", "option2", "option3"] guard allowedValues.contains(userInput) else { return } -
Base64 encoding: Encode arbitrary values to prevent injection:
swift let encoded = Data(userInput.utf8).base64EncodedString() -
Server-side sessions: Store sensitive data on the server, not in cookies:
swift req.session.data["userId"] = userId // Stored server-side // Cookie only contains random session ID -
Character blacklisting (as last resort): Block dangerous characters:
swift let dangerousChars = CharacterSet(charactersIn: ";,=\r\n") guard value.rangeOfCharacter(from: dangerousChars) == nil else { return nil } -
Pattern validation: Use regex to enforce strict formats:
swift let pattern = "^[a-zA-Z0-9_-]{1,50}$" // Validate userInput matches pattern -
Never trust user input: Always validate/sanitize before using in cookies
-
Use secure cookie attributes: Set
Secure,HttpOnly, andSameSiteflags
Configuration
The detector has the following configurable parameters:
-
sources, that indicates the source kinds to check. -
neutralizations, that indicates the neutralization kinds to check.
Unless you need to change the default behavior, you typically do not need to configure this detector.
References
-
CWE-472 : External Control of Assumed-Immutable Web Parameter.
-
OWASP - Top 10 2021 Category A04 : Insecure Design.
-
CAPEC-31 : Accessing / Intercepting / Modifying HTTP Cookies.