Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Malicious site protection navigation detection #3707

Open
wants to merge 16 commits into
base: alessandro/malicious-site-protection
Choose a base branch
from

Conversation

alessandroboron
Copy link
Contributor

@alessandroboron alessandroboron commented Dec 10, 2024

Task/Issue URL: https://app.asana.com/0/1206329551987282/1207151848931030
Tech Design URL: https://app.asana.com/0/1206329551987282/1207273224076495/f
Cc: @not-a-rootkit

Description:

This PR adds the navigation logic for detecting a malicious site and navigating to a special error page if the site is malicious.

The original idea in the tech design was to intercept the Request in decidePolicyForNavigationAction and check whether a site is malicious cancelling the request accordingly.

We noticed that the above approach increases the page load time of websites due to the logic check.

I opted for an approach where in decidePolicyForNavigationAction we start the detection task in parallel without waiting and in decidePolicyForNavigationResponse we evaluate the task’s result.

Another approach I thought of was to perform the logic in the background in didStartProvisionalNavigation. The problem with this approach is that is called only for navigation that starts from the main frame so it would not be possible to intercept malicious iFrame URLs.

NOTE
BSK Threat detection is currently mocked. Its integration and tests will follow in an upcoming PR.

Steps to test this PR:

Scenario 1 - Phishing Website should show special error page

  1. Ensure that http://privacy-test-pages.site/security/badware/phishing.html shows the phishing special error page with the copy “This website may be impersonating a legitimate site...".

Scenario 2 - Malware Website should show special error page

  1. Ensure that http://privacy-test-pages.site/security/badware/malware.html shows the malware special error page with the copy “DuckDuckGo blocked this page because it may be distributing malware...".

Scenario 3 - Allow visit a malicious website should not show the error again if navigating back to the site

  1. Visit one of the malicious sites above.
  2. Tap the “Advanced” button
  3. Tap the “Accept Risk and Visit Site” link
  4. Wait for malicious site to load.
  5. Refresh page.
  6. Ensure the website load without showing the error again.
  7. Close tab.
  8. Paste the same URL again.
  9. Ensure the special error page is shown again.

Scenario 4 - Leave a malicious site should close the Tab

  1. Visit one of the malicious sites above.
  2. Tap the “Leave Site” button.
  3. Ensure that the tab is closed.

Definition of Done (Internal Only):

Copy Testing:

  • Use of correct apostrophes in new copy, ie rather than '

Orientation Testing:

  • Portrait
  • Landscape

Device Testing:

  • iPhone SE (1st Gen)
  • iPhone 8
  • iPhone X
  • iPhone 14 Pro
  • iPad

OS Testing:

  • iOS 15
  • iOS 16
  • iOS 17

Theme Testing:

  • Light theme
  • Dark theme

Internal references:

Software Engineering Expectations
Technical Design Template

Copy link

github-actions bot commented Dec 10, 2024

Warnings
⚠️ PR has more than 500 lines of code changing. Consider splitting into smaller PRs if possible.

Generated by 🚫 dangerJS against 5c07c05

@@ -26,11 +27,14 @@ import Foundation
/// advanced information related to the error.
protocol SpecialErrorPageActionHandler {
/// Handles the action of navigating to the site associated with the error page
func visitSite()
@MainActor
func visitSite(url: URL, errorData: SpecialErrorData)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m a bit on the fence with this.
The idea was to have a generic protocol that all the sub-special error handlers should conform to.
For MaliciousSiteProtectionNavigationHandler I need to pass the URL and SpecialErrorData but I don’t need them for the SSL handler. So I feel I’m violating the Interface Segregation principle. what do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you perhaps create two funcs visitSite, one without params and one with params? Different handlers would be implementing only one function leaving other implementation empty? It's not ideal but the simplest

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, if we encounter more cases like this where we’re stretching these protocols, we might need to reconsider whether it’s appropriate to treat all special errors the same way. For example, SSL and malicious site errors already exhibit different behaviors. I’ll leave it up to you, but perhaps we should treat them differently (not under same logic) from the start. We can discuss further in 2025 :)

let response = MaliciousSiteDetectionNavigationResponse(navigationAction: navigationAction, errorData: errorData)
return .navigationHandled(.mainFrame(response))
} else {
// Extract the URL of the source frame (the iframe) that initiated the navigation action
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will look more into iFrame logic when I integrate with BSK


@MainActor
func handleMaliciousExemptions(for navigationType: WKNavigationType, url: URL) {
// TODO: check storing redirects
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will address this TODO in upcoming PR

@@ -90,7 +94,7 @@ extension SSLErrorPageNavigationHandler: SpecialErrorPageActionHandler {
Pixel.fire(pixel: .certificateWarningLeaveClicked)
}

func visitSite() {
func visitSite(url: URL, errorData: SpecialErrorData) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alessandroboron alessandroboron force-pushed the alessandro/malicious-site-protection-navigation-detection-async branch from 8f0b90f to b6dbf5c Compare December 10, 2024 13:06
Copy link

This PR has been inactive for more than 7 days and will be automatically closed 7 days from now.

@github-actions github-actions bot added the stale label Dec 18, 2024
@alessandroboron alessandroboron force-pushed the alessandro/malicious-site-protection branch from ef5bd3f to e2f654c Compare December 19, 2024 12:36
@alessandroboron alessandroboron force-pushed the alessandro/malicious-site-protection-navigation-detection-async branch from b6dbf5c to 5c07c05 Compare December 19, 2024 12:42
@github-actions github-actions bot removed the stale label Dec 20, 2024
Comment on lines +29 to +31
case "http://privacy-test-pages.site/security/badware/phishing.html":
return .phishing
case "http://privacy-test-pages.site/security/badware/malware.html":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it be http vs https?
also, we should be keeping all urls inside AppURLs

@@ -26,11 +27,14 @@ import Foundation
/// advanced information related to the error.
protocol SpecialErrorPageActionHandler {
/// Handles the action of navigating to the site associated with the error page
func visitSite()
@MainActor
func visitSite(url: URL, errorData: SpecialErrorData)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you perhaps create two funcs visitSite, one without params and one with params? Different handlers would be implementing only one function leaving other implementation empty? It's not ideal but the simplest

/// - Returns: A Boolean value that indicates whether the navigation action was handled.
func handleSpecialErrorNavigation(navigationAction: WKNavigationAction, webView: WKWebView) async -> Bool
@MainActor
func handleDecidePolicyFor(navigationAction: WKNavigationAction, webView: WKWebView)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be
func handleDecidePolicy(for navigationAction: WKNavigationAction, webView: WKWebView)

/// - webView: The web view from which the navigation request began.
/// - Returns: A Boolean value that indicates whether to cancel or allow the navigation.
@MainActor
func handleDecidePolicyfor(navigationResponse: WKNavigationResponse, webView: WKWebView) async -> Bool
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo, "for" is lowercased, but I'd refer to my previous comment ^

/// the navigation event.
/// - webView: The web view from which the navigation request began.
@MainActor
func createMaliciousSiteDetectionTask(for navigationAction: WKNavigationAction, webView: WKWebView)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd go with makeMalicious*

return
}

let threatDetectionTask: Task<MaliciousSiteProtectionNavigationResult, Never> = Task.detached { [weak self] in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is the Task detached?

Comment on lines +84 to +90
guard let url = navigationAction.request.url else {
return
}

if let aboutBlankURL = URL(string: "about:blank"), url == aboutBlankURL {
return
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we put it under one guard, like so
guard let url = navigationAction.request.url, url != URL(string: "about:blank") else { return }

@@ -26,11 +27,14 @@ import Foundation
/// advanced information related to the error.
protocol SpecialErrorPageActionHandler {
/// Handles the action of navigating to the site associated with the error page
func visitSite()
@MainActor
func visitSite(url: URL, errorData: SpecialErrorData)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, if we encounter more cases like this where we’re stretching these protocols, we might need to reconsider whether it’s appropriate to treat all special errors the same way. For example, SSL and malicious site errors already exhibit different behaviors. I’ll leave it up to you, but perhaps we should treat them differently (not under same logic) from the start. We can discuss further in 2025 :)

self.startDownload(with: navigationResponse, decisionHandler: decisionHandler)
} cancelHandler: {
// If the navigation has been handled by the special error page handler cancel navigating to new content as the special error page will be shown.
Task { @MainActor in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoa :), this is triggering an alert for me. Making a task on this code could have bigger consequences – we’re essentially returning immediately and skipping the run loop, without a guarantee that other tasks spawned elsewhere won’t interfere with this. Are we sure this is the approach we want to take?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants