Content filter providers seem to block all requests during startup on iOS 16 or 17

Hi,

Consider a content filter app that allows most requests. While running, it handles requests and determine what to do. It does not affect any request while it is not running.

However, during startup, it seems to block all requests, on iOS 16 or 17.

On iOS 15, the behavior is bit different. Show the picture below:

Questions

  • Is this the expected behavior?
  • Is this documented?

Steps to reproduce

  1. Create content filter app with filter data provider with lengthy startup, something like the following:

    import NetworkExtension
    
    class FilterDataProvider: NEFilterDataProvider {
        override func startFilter(completionHandler: @escaping ((any Error)?) -> Void) {
            Task {
                try await Task.sleep(nanoseconds: 10 * 1_000_000_000)
                completionHandler(nil)
            }
        }
    
        override func stopFilter(with reason: NEProviderStopReason) async {}
    
        override func handleNewFlow(_ flow: NEFilterFlow) -> NEFilterNewFlowVerdict {
            guard let url: String = flow.url?.absoluteString else {
                return .allow()
            }
    
            if url.contains("example.net/") {
                return .drop()
            }
    
            if url.contains("example.org/") {
                exit(42)
            }
    
            return .allow()
        }
    }
    
  2. Install the app on a supervised iPhone or iPad.

  3. Install a WebContentFilter profile.

  4. Wait for the content filter to start. You can check the status in Settings > General > VPN & Device Management > Content Filter.

  5. Open Safari app.

  6. Request http://example.net/ and confirm that it is blocked.

  7. Request the other URLs and confirm that it is allowed.

  8. Request http://example.org. It kills the filter data provider.

  9. Request some URLs quickly.

Background to the questions

We offer a content filter app that might be stopped during the device sleeps. When a non-our-app’s push notification is received, the device wakes up, and the content filter starts up. Then the push notification seems to be lost. It is observed on iOS 16 and 17, not on iOS 15.

Answered by DTS Engineer in 797205022

Is this the expected behavior?

Yes and, in fact, I believe the iOS 15 behavior was in fact a bug. In security terms, iOS 15 is "failing open", which means it's possible to bypass your content filter by indirectly disturbing it's startup process (for example, but blocking or stalling specific requests).

Is this documented?

No. The current behavior is how this kind of API is expected to behave and the iOS 15 behavior was a bug.

We offer a content filter app that might be stopped during the device sleeps. When a non-our-app’s push notification is received, the device wakes up, and the content filter starts up. Then the push notification seems to be lost. It is observed on iOS 16 and 17, not on iOS 15.

If you want to reimplement the iOS 15 behavior, then you can modify your start logic to "finish" as quickly as possible and then simply allow all flows until you've finished your own initialization and can start blocking again.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Accepted Answer

Is this the expected behavior?

Yes and, in fact, I believe the iOS 15 behavior was in fact a bug. In security terms, iOS 15 is "failing open", which means it's possible to bypass your content filter by indirectly disturbing it's startup process (for example, but blocking or stalling specific requests).

Is this documented?

No. The current behavior is how this kind of API is expected to behave and the iOS 15 behavior was a bug.

We offer a content filter app that might be stopped during the device sleeps. When a non-our-app’s push notification is received, the device wakes up, and the content filter starts up. Then the push notification seems to be lost. It is observed on iOS 16 and 17, not on iOS 15.

If you want to reimplement the iOS 15 behavior, then you can modify your start logic to "finish" as quickly as possible and then simply allow all flows until you've finished your own initialization and can start blocking again.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Thank you for quick reply.

If you want to reimplement the iOS 15 behavior, then you can modify your start logic to "finish" as quickly as possible and then simply allow all flows until you've finished your own initialization and can start blocking again.

Although even the fastest start logic (empty startFilter() async) seem to affect incoming events rarely in my observation, it helps me a lot.

Content filter providers seem to block all requests during startup on iOS 16 or 17
 
 
Q