Let me start off by saying I absolutely love Swift Package Manager and I think it’s the future of dependency management on iOS, so-much-so I’ve written posts on what we can do with it.
But I also want to use this opportunity to highlight a potential risk with using it that I became aware of today. I initially thought there was no issue tracker for Swift Package Manager since there isn’t one in the GitHub repo, but I’ve since reported this issue on the Swift bug tracking Jira instance (SR-13346).
I’ve found a way to run any code I want when you use one of my Swift packages (providing you don’t check the Package.swift file before hand), and it’s a lot simpler than you might think. Let me show you how.
It’s a very, very simple hack, making use of anonymous closures to lazily load the
Package instance defined in the Package.swift file for a package.
UPDATE: After writing this post and filing a bug with the Swift team I realised even this is more complicated that it needs to be. You can simply create a function that returns anything that the Package instance needs (like a string for the name) and execute any other code you want before returning.
First, let’s look at a normal, simple Package.swift file that gets generated when you run
swift package generate:
When you add this package as a dependency, Swift Package Manager does some magic by looking for the
package instance in this file.
Since this file is just a regular Swift file, we can write any Swift code we want in here, including tricking Swift Package Manager to call a function whenever the
package instance is referenced:
The code above has hardly been changed, but can now be used to run any code we want in
doSomething(). Let’s add some code to
doSomething that makes your Mac speak out loud (yes really!):
In the above code, we’ve updated
doSomething so it now runs an executable file called
say which speaks out any words you give it as an argument (included in every Mac by default).
Notice how we’re able to literally execute a programme that is stored in
usr/bin. To do this we have to use a
Process instance, which is part of
AppKit, which we have no problems importing into our Package.swift file, because it’s just Swift code running on a Mac.
If you add our package as a dependency to another package, or as a dependency in an Xcode project, you’ll hear your Mac say “hello world” whenever the package is resolved. In fact, you only have to save the Package.swift file to test it and it’ll speak.
This gets more worrying when you add this package as a remote dependency in Xcode (11 or 12), because the code runs as soon as you add it as a dependency:
By the time you get to that screenshot above in Xcode 11 or 12, the function has already been called and your Mac has said “hello world” out loud.
Just being able to import AppKit opens up the possibility of so many things, both malicious and not. As an example, with AppKit you can get a list of running apps, terminate them and launch new processes with NSWorkspace.
We can import other macOS frameworks too, like Collaboration, which lets us get information like the full name of the current user, and more alarmingly, test to see if a password is the correct password:
If “notmypassword” happens to be your actual login password for your Mac, the above code will speak out:
“Hello [your name], notmypassword is your password”
Even without importing AppKit or any of the other macOS frameworks, we can import Foundation which allows us to make any network requests we want:
The above code uses a
DispatchGroup to make the thread wait until the network call has completed. This network call could be used to download any data from any site, and upload any data anywhere.
If that wasn’t enough, since you can access
FileManager from Foundation, you can also write any data to disk, as well as read directly from the filesystem, or delete files. In theory, you could delete the entire user directory (although I was too chicken to try!).
The above code iterates through the entire filesystem, logging each item to the console. Thankfully in Catalina you do have to give Xcode permission to access some directories, but that just confirms that you’ll be able to run code in any privileged way that Xcode does. I’m also subconciously trained to just accept any permissions requests that Xcode throws up at me.
These are just a few examples off the top of my head of what we can do with this, I’m sure there are plenty more that cleverer people can think of.
What can you do to protect yourself from people who want to abuse this vulnerability?
Well, unfortunately you can’t entirely, but you can help protect yourself by checking each 3rd party Package.swift file before using it, and make sure you target a specific version in a remote repo rather than always resolving the latest version of a branch. Remember though there’s nothing stopping someone from changing the code that a tag in a remote repo references.
Additionally, you should only use packages from developers you trust. Unfortunately this could be an unfair disadvantage for new developers with something cool they want to share via Swift Package Manager, and in any case there’s still a risk (albeit small) reputable developers could have their repositories compromised.
Hopefully, GitHub will be able to add some vulnerability checks for Swift packages like they do with other language dependencies in the future, and of course hopefully the Swift developers find a way to plug this properly without impacting the usefulness of either Swift Package Manager or system frameworks.
In the interests of fairness, I should also point out that Cocoapods has a way of running scripts as the framework is being built, however they at least choose to log warnings to developers so it’s not completely silent. And they expose it as an official feature so people are more aware of it.
You should also make sure you’re not storing anything sensitive on disk outside of the Keychain, that means no storing passwords in unencrypted text files, no leaving private keys or certificates on the desktop etc.
Additionally, although I was able to write code that retrieves private keys from the keychain (or references to keys, more specifically), I wasn’t able to actually use the key to sign data without a permissions prompt coming up:
This is why it’s important not to blindly allow permissions for things that pop up, even if they seem like they’re coming from the system. For example, using some keychain code I was able to get this popup to appear while trying to use private keys:
That one is particularly worrying because it doesn’t mention my package name anywhere, and since it’s saying macOS wants to make changes I would be tempted to allow it if I didn’t know it was my code that caused it. I don’t know what entering my username and password does in this case, because again I was too chicken to try (maybe I can set up a demo machine so I can properly try this stuff without worrying!).
It’s not all bad
If you’ve read this far you might be worried (and rightly so), but it’s also worth pointing out that although this can be used for evil, it can also be used for good.
This sort of thing could enable code generation (which is why I thought of this in the first place), or providing some required automatic setup for users of libraries that have a difficult setup process (like adding build phase scripts for Firebase).
Sadly, we live in a world where bad actors ruin things and That’s Why We Can’t Have Nice Things™️.
How can the Swift team fix it?
I’m honestly not sure the best way for the team to fix it, if Swift code is going to be allowed in full then this will always happen. Perhaps disallowing imports or functions when parsing the Package.swift file could work.
I’ve created an example repository which you can check out if you want to see it for real.