URLSession can access password protected directory without credentials

I am trying to download app content from a password protected directory of a website served by Apache24. The directory is protected using the following configuration segment:

<Directory "<directory path">
	AuthType Basic
	AuthName "Restricted Content"
	AuthUserFile <password file path>.htpasswd
	Require valid-user
</Directory>

Here is my swift code (running on latest betas of iOS15 or macOS12)

class Downloader: NSObject {

	lazy var downloadSession: URLSession = {
		
		// Setup configuration
		let configuration = URLSessionConfiguration.default
		configuration.allowsCellularAccess = true
		configuration.timeoutIntervalForResource = 60
		configuration.waitsForConnectivity = true
		
		// Add authorisation header to handle credentials
		let user = "*****"
		let password = "******"
		let userPasswordData = "\(user):\(password)".data(using: .utf8)
		let base64EncodedCredential = userPasswordData!.base64EncodedString(options: Data.Base64EncodingOptions.init(rawValue: 0))
		let authString = "Basic \(base64EncodedCredential)"
		
		// Add authorisation header to configuration
		//configuration.httpAdditionalHeaders = ["Authorization" : authString]

		return URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
	}()
	
	// Download file using async/await
	func downloadAsync(subpath: String) async throws {
		let request = URLRequest(url: URL(string: "https://<server>/")!)
		let (data, response) = try await downloadSession.data(for: request)
		guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw HTTPError.withIdentifier((response as! HTTPURLResponse).statusCode) }
		print(String(data: data, encoding: .utf8))		
	}
}

let downloader = Downloader()
Task.init {
	do {
		try await downloader.downloadAsync(subpath: "<filename>")
	} catch {
		print("Unable to download file")
	}
}
  • As expected, if I run the code as is (with the authorisation header commented out) it does not download the file

  • As expected, if I then uncomment the authorisation line, and run it again, it DOES download the file

Here is the unexpected part (to me!):

  • If I re-comment out the authorisation line, and run it again it STILL downloads the file

  • This can be repeated for several minutes, before it finally refuses to download the file

  • The issue occurs on both iOS and macOS

There is a clear gap in my understanding here about what is going on, so my questions are:

  1. What is causing this behaviour? A session cookie on the client, or something on the server?
  2. Does it represented a security risk? (Could another client without credentials download the file shortly after a legitimate download)
  3. If the answer to 2 is YES, how do I stop it?

Many thanks,

Bill Aylward

Replies

Here is my swift code

FYI, this is not correct. Rather than manually construct an Authorization header you should instead respond to the NSURLAuthenticationMethodHTTPBasic authentication challenge. For more on this, see Handling an Authentication Challenge.

However, this won’t affect your core issue.

Here is the unexpected part (to me!)

I suspect the response is coming out of the cache. To confirm this, implement the task metrics delegate callback and look at the resourceFetchType property.

If you don’t want the request to come from the cache, construct it like so:

URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 60.0)

Share and Enjoy

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

Thanks Quinn, it was coming out of the cache, and adding your suggested cachePolicy fixes the issue.

Incidentally, when I set up a delegate to handle an authentication challenge like this:

func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
	print("didReceive challenge \(challenge.protectionSpace.authenticationMethod), failure count: \(challenge.previousFailureCount)") 	completionHandler(.useCredential, nil)
}

func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
	print("Task: \(task.taskIdentifier) didReceive challenge \(challenge.protectionSpace.authenticationMethod)") 	completionHandler(.useCredential, nil)
}

The output is:

didReceive challenge NSURLAuthenticationMethodServerTrust, failure count: 0

didReceive challenge NSURLAuthenticationMethodServerTrust, failure count: 0

I'm not sure why I am not getting a NSURLAuthenticationMethodHTTPBasic challenge?

Bill Aylward

I'm not sure why I am not getting a NSURLAuthenticationMethodHTTPBasic challenge?

Check out Listing 1 and the associated text in the doc I referenced. In short, if you get a challenge that you don’t care about — in this case that’s the server trust challenge — resolve it with .performDefaultHandling. This will allow the connection to proceed and generate the HTTP Basic challenge.

Share and Enjoy

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