Memory leak using swift JSONEncoder. serialized data is still left in app's memory after logout [iOS]

I run memory dump on my app's memory and find out that it still holds some
critical data that was not supposed to stay there after logout.

(note: I have generated the memory dump while in debug mode).

after some investigation I have found out that it is actually the full JSON I am sending via
Almofire (HTTP network library) that is still in memory.
I suspected the issue is with the JSONEncoder

so I have created a small app having a simple button that run the next code:

    func encodeJson() {
        let encoder = JSONEncoder()
        
        var dic: [String: String] = [
            "email": "someMail@email.com",
            "password": "somePassord",
            "appName": "test app",
            "version": "1.1.1",
            "os": "ios",
        ]
        
    
        do {
            let data = try encoder.encode(dic)
            dic = [:]
        } catch {
            
        }
        
    }

after clicking the button multiple times in a row, I found that the JSON is still in memory (the serialized json, not the dictionary) even after
the function finished running!

I feel like it is an open door tempts the thief!
Is there a known memory leak with JSONEncoder?

Is there a different reasoning to the issue that I am missing?

I have read that developers were complaining about related issues with JSONEncoder but didn't find any solutions.

the reason why it is a security issue:
in a jail broken device, one can stole critical data after user have logged out after installing a fishing app.

  • Data remaining in formerly-malloc’d memory after it has been freed is not the same thing as a memory leak. It’s only a memory leak if malloc thinks the memory is still in use (e.g. nobody has called free on it) but nobody has a pointer to that memory.
  • Jailbreaking by definition involves local privilege escalation. You cannot defend against someone using that escalation to read your process’s memory.

There are legitimate use cases for removing values from memory, but generally speaking you have to handle that memory using very specific APIs, and once you read any values from that memory into a variable, you must consider it lost.

2 Likes

@ksluder Thanks for your answer,
you don't consider the issue I described as legitimate use case because one have to jail broke it's iPhone to make a use of it?
What are those specific APIs you suggesting to use in case I believe I have to get rid of the data?

While we speak I have found another similar issue in my app. this occurs while reading\writing to the keyChain using SecItemCopyMatching / SecItemAdd - I actually see the all keyChain in memory!! (not consistently though) using this code:

 public func readDataFromKeyChain() -> String? {
        var dataToReturn: String = ""
        var keychainQuery: [String: Any] = [:]
        keychainQuery[String(kSecClass)] = kSecClassGenericPassword
        keychainQuery[String(kSecAttrService)] = "someService"
        keychainQuery[String(kSecReturnData)] = kCFBooleanTrue
        keychainQuery[String(kSecAttrSynchronizable)] = kCFBooleanFalse
        
        
        var result: AnyObject?
        let status = SecItemCopyMatching(keychainQuery as CFDictionary, &result)
        
        if status == noErr,
           let data = result as? Data {
            let dataRead = String(data: data, encoding: .utf8)
            print("read successfuly")
            print("data read:\(dataRead)")
            return dataRead
        } else {
            print("error occured while reading")
        }
        return dataToReturn
    }

I was considering to open a separate post for the code above, but I am curious to know if you think it might be due to the same reasoning? (i.e just data remaining and wasn't freed in the process)

I wouldn’t call your case “illegitimate”. There are reasons to want to protect certain values from extraction. The issue is that jailbreaking necessarily involves tricking the device into trusting unsigned code running in a privileged context. If the user has that ability, they can use it to take over your process and inject code that t the secrets.

The typical answer is “this why the Secure Element exists.” The OS can’t see the memory of the Secure Enclave; it can only communicate via a limited mailbox API. The Secure Enclave itself is running a hardened kernel; I don’t know much about what techniques or mitigations that kernel uses to prevent data exfiltration.

It is common for APIs that work with a database to map the database using shared memory objects.

1 Like

so considering your answers, I think my only practical solution is to encrypt the data I want to protect throughout the app's lifetime - putting it this way in the keyChain and also sending it to the server this way. than the server should decrypt it :man_shrugging:

Not really, considering that, by your definition, the unencrypted values will still remain in memory since you need to decrypt them to access them. For protection in transit you have TLS. Even being careful to access keychain values directly from the API and not store them directly, at some point the value must enter memory unprotected so the rest of your app can use it. If you app doesn't need the plain text value you can simply store the encrypted value and pass it around, but that value is still subject to local disclosure. In the general case there is no way to protect against privileged access to your process.

2 Likes

Have you tried it in release mode? Could be the case that in debug mode free doesn't zero released memory and in release mode it does.

I take it this question is not about the memory leak but about a potential security issue? Or do you mean that if I call the above call many times in a loop, used memory amount will grow indefinitely?

1 Like

I don’t think that’s practical. What data are you trying to protect? Remember that whenever you type a password into a text field, it is sitting in unprotected memory. When you unlock the Passwords settings pane, all those labels containing your usernames and passwords store their contents in unprotected memory.

On Unix-like platforms, the process is usually considered the fundamental security barrier. Everything within a process is trusted.

2 Likes

Tera thanks for your questions!
first, as far I know, the only way to generate a memory dump is in debug mode, while using the LLDB.
I don't have a jail broken device to generate it on release mode.
but you raise a very interesting question!
regarding memory leak vs security issue - I didn't check if the memory grows indefinitely, I was calling
it multiple times just to increase probability for it to reproduce.
but looking at the memory dump, I don't see multiple instances of this JSON, so I do believe it is
only a security issue, do you think I should change the post title?

I agree that once the user type the password in the text field it is sitting in unprotected memory.
but thats about the only place I have no control over it. From this step forward, I can encrypt it and save it this way on the keyChain and serialize it quietly without worrying of it staying in memory.
in other words, I can decrease the chances of the issue dramatically.

@Jon_Shier thanks for your answer!
I answered @ksluder to some of the points you raised here.
I will emphasize that my issue is not in transit, but in the serialization process it self (jsonEncode)
which leaves the data in memory.
with password for example, I don't see why the value must enter to memory unprotected (apart from the textField) - I can send it encrypted to the server letting them decrypt it. considering that, why the value should be subject to disclosure?