Outgoing XPC message goes through to untrusted Peer

I have run into an interesting topic today.

So far, I have been under the impression that when I am using the setCodeSigningRequirement() function on an NSXPCConnection, I am completely removing any chance of receiving AND sending messages to untrusted XPC Peers.

However, I created a malicious replacement for my daemon, and I wanted to check if my application can still send and receive messages to it.

  • I checked with codesign --verify that the replacement does NOT fulfil the code signing requirement.
  • I put a system log instruction in the malicious tool's XPC function.

When calling the XPC Peer, I expected to see:

XPC connection to <redacted> failed! [Error Domain=NSCocoaErrorDomain Code=4102 "The code signature requirement failed." UserInfo={NSDebugDescription=The code signature requirement failed.}]

and I did.

However, I also saw the system log from the malicious tool's XPC function.

Then, I checked all XPC documentation, and I found for the original C implementation - xpc_connection_set_peer_code_signing_requirement() - the following in the discussion section:

All messages received on this connection will be checked to ensure they come from a peer who satisfies the code signing requirement. For a listener connection, requests that do not satisfy the requirement are dropped. When a reply is expected on the connection and the peer does not satisfy the requirement XPC_ERROR_PEER_CODE_SIGNING_REQUIREMENT will be delivered instead of the reply.

(this is in xpc/connection.h)

which seems to align with the observed results. However, this is (embarassingly?) new for me, I would have never expected this, given how in my head pre-checking before any connection is made seems straightforward, even with public Apple SDK APIs:

  1. Grab a SecCode (not SecStaticCode) object of the daemon (malicious or not). This is running code, so it cannot be substituted between the check and the outgoing message.
  2. Perform validations on the SecCode object in some form - on macOS 15.0+ it's pretty easy with LightweightCodeRequirement's SecCodeCheckValidityWithProcessRequirement().
  3. Immediately drop the connection if the peer is untrusted, before any message is sent.

Am I overlooking something or making wrong assumptions here?

or

Am I right and this is something that I have to accept that's implemented less than ideally and I can perform above steps 1-3 myself and make a difference?

Thanks in advance!

Answered by DTS Engineer in 896946022
I also saw the system log from the malicious tool's XPC function.

Right. That’s what I’d expect based on my understanding of how XPC works, but I agree that this is non-obvious |-:

Most folks don’t run into this because they don’t implement a client-authenticates-server check. Rather:

  • The server authenticates the client.
  • And the client sets the privileged flag (see this post) to ensure that the server is in the global namespace.

That second step is a reasonable level of security IMO. Keep in mind that it’s basically impossible for a non-privileged client to protect itself from a privileged server. The server could, for example, rewrite the client’s code to remove any checks that you implement in the client.

given how in my head pre-checking before any connection is made seems straightforward

Your confusion stems from the fact that XPC has a connection-oriented API but the underlying transport, Mach messaging, is not connection-oriented. So, you can’t run this check on the connection, because there isn’t one. Rather, you have to run this check on a received message. That received message includes an audit token in a message trailer, and it’s that audit token that the system checks.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

I also saw the system log from the malicious tool's XPC function.

Right. That’s what I’d expect based on my understanding of how XPC works, but I agree that this is non-obvious |-:

Most folks don’t run into this because they don’t implement a client-authenticates-server check. Rather:

  • The server authenticates the client.
  • And the client sets the privileged flag (see this post) to ensure that the server is in the global namespace.

That second step is a reasonable level of security IMO. Keep in mind that it’s basically impossible for a non-privileged client to protect itself from a privileged server. The server could, for example, rewrite the client’s code to remove any checks that you implement in the client.

given how in my head pre-checking before any connection is made seems straightforward

Your confusion stems from the fact that XPC has a connection-oriented API but the underlying transport, Mach messaging, is not connection-oriented. So, you can’t run this check on the connection, because there isn’t one. Rather, you have to run this check on a received message. That received message includes an audit token in a message trailer, and it’s that audit token that the system checks.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Outgoing XPC message goes through to untrusted Peer
 
 
Q