Siam Thanat Hack Co., Ltd.

OWASP Mobile Top 10:2024

A guide to the ten current OWASP Mobile Top 10:2024 categories for Android, iOS, and the services they depend on.

Edition 2024; risk names follow the official OWASP publication.

M1

Improper Credential Usage

Improper Credential Usage means a mobile app embeds, stores, transmits, or uses passwords, API keys, tokens, or sessions unsafely. Hardcoded secrets or passwords in ordinary device storage can be extracted through reverse engineering or a lost device and reused against the backend.

Risk at a glance
What can go wrongSecrets, tokens, API keys, or passwords are embedded, reused, exposed, or handled without an appropriate lifecycle.
Business effectA recovered application package or leaked token can provide ongoing access to a backend or protected function.
First control to verifyKeep long-lived secrets out of the client.

Attacker Victim Application Security control

M1 attack path against a victim application and its mitigating control

The diagram contrasts the unsafe route with the control that interrupts it. Select the image to inspect it at full size.

Why it matters

A recovered application package or leaked token can provide ongoing access to a backend or protected function.

Risk-reduction focus

  • Keep long-lived secrets out of the client.
  • Use scoped, revocable credentials and rotate them.
  • Protect credential entry and transmission paths.
Android example in Kotlin

Vulnerable example

import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity

class RedirectActivity : AppCompatActivity() {
    fun openRedirect(intent: Intent) {
        val rawTarget = intent.data?.getQueryParameter("redirect") ?: return
        startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(rawTarget)))
    }
}

Fixed example

import android.content.Intent
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import androidx.appcompat.app.AppCompatActivity

class RedirectActivity : AppCompatActivity() {
    private val allowedHosts = setOf("app.example.com")

    fun openRedirect(intent: Intent) {
        val rawTarget = intent.data?.getQueryParameter("redirect") ?: return
        val target = Uri.parse(rawTarget)
        if (target.scheme != "https" || target.host !in allowedHosts) return
        CustomTabsIntent.Builder().build().launchUrl(this, target)
    }
}
iOS example in Swift

Vulnerable example

import UIKit

final class RedirectViewController: UIViewController {
    func openRedirect(rawTarget: String) {
        guard let target = URL(string: rawTarget) else { return }
        UIApplication.shared.open(target)
    }
}

Fixed example

import SafariServices
import UIKit

final class RedirectViewController: UIViewController {
    private let allowedHosts: Set<String> = ["app.example.com"]

    func openRedirect(rawTarget: String) {
        guard let target = URL(string: rawTarget),
              target.scheme == "https",
              let host = target.host,
              allowedHosts.contains(host) else { return }

        let browser = SFSafariViewController(url: target)
        present(browser, animated: true)
    }
}

M2

Inadequate Supply Chain Security

Inadequate Supply Chain Security means the app, SDKs, dependencies, build system, or package signing lack adequate provenance and integrity control. A changed third-party SDK, compromised pipeline, or leaked signing key can make an installed app do work the team never intended.

Risk at a glance
What can go wrongMobile SDKs, build plugins, signing systems, and distribution dependencies are not sufficiently governed.
Business effectA compromised SDK or release pipeline can introduce tracking, data exposure, or altered application behavior.
First control to verifyInventory third-party SDKs and their data flows.

Attacker Victim Application Security control

M2 attack path against a victim application and its mitigating control

The diagram contrasts the unsafe route with the control that interrupts it. Select the image to inspect it at full size.

Why it matters

A compromised SDK or release pipeline can introduce tracking, data exposure, or altered application behavior.

Risk-reduction focus

  • Inventory third-party SDKs and their data flows.
  • Restrict and audit signing and release access.
  • Verify package origin, integrity, and update notices.
Android example in Kotlin

Vulnerable example

import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity

class RedirectActivity : AppCompatActivity() {
    fun openRedirect(intent: Intent) {
        val rawTarget = intent.data?.getQueryParameter("redirect") ?: return
        startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(rawTarget)))
    }
}

Fixed example

import android.content.Intent
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import androidx.appcompat.app.AppCompatActivity

class RedirectActivity : AppCompatActivity() {
    private val allowedHosts = setOf("app.example.com")

    fun openRedirect(intent: Intent) {
        val rawTarget = intent.data?.getQueryParameter("redirect") ?: return
        val target = Uri.parse(rawTarget)
        if (target.scheme != "https" || target.host !in allowedHosts) return
        CustomTabsIntent.Builder().build().launchUrl(this, target)
    }
}
iOS example in Swift

Vulnerable example

import UIKit

final class RedirectViewController: UIViewController {
    func openRedirect(rawTarget: String) {
        guard let target = URL(string: rawTarget) else { return }
        UIApplication.shared.open(target)
    }
}

Fixed example

import SafariServices
import UIKit

final class RedirectViewController: UIViewController {
    private let allowedHosts: Set<String> = ["app.example.com"]

    func openRedirect(rawTarget: String) {
        guard let target = URL(string: rawTarget),
              target.scheme == "https",
              let host = target.host,
              allowedHosts.contains(host) else { return }

        let browser = SFSafariViewController(url: target)
        present(browser, animated: true)
    }
}

M3

Insecure Authentication/Authorization

Insecure Authentication/Authorization means the mobile app or backend misidentifies a user or grants actions incorrectly. Incomplete token checks, client-supplied roles, or sessions surviving logout can enable account takeover, cross-user data access, or restricted functions.

Risk at a glance
What can go wrongThe app or its backend accepts weak identity proof, session handling, or permission decisions.
Business effectA device user or remote actor can access another account, protected feature, or data set.
First control to verifyAuthorize requests on the backend, not only in the app.

Attacker Victim Application Security control

M3 attack path against a victim application and its mitigating control

The diagram contrasts the unsafe route with the control that interrupts it. Select the image to inspect it at full size.

Why it matters

A device user or remote actor can access another account, protected feature, or data set.

Risk-reduction focus

  • Authorize requests on the backend, not only in the app.
  • Use secure sessions and risk-appropriate MFA.
  • Test deep links, app components, and backend object access.
Android example in Kotlin

Vulnerable example

import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity

class RedirectActivity : AppCompatActivity() {
    fun openRedirect(intent: Intent) {
        val rawTarget = intent.data?.getQueryParameter("redirect") ?: return
        startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(rawTarget)))
    }
}

Fixed example

import android.content.Intent
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import androidx.appcompat.app.AppCompatActivity

class RedirectActivity : AppCompatActivity() {
    private val allowedHosts = setOf("app.example.com")

    fun openRedirect(intent: Intent) {
        val rawTarget = intent.data?.getQueryParameter("redirect") ?: return
        val target = Uri.parse(rawTarget)
        if (target.scheme != "https" || target.host !in allowedHosts) return
        CustomTabsIntent.Builder().build().launchUrl(this, target)
    }
}
iOS example in Swift

Vulnerable example

import UIKit

final class RedirectViewController: UIViewController {
    func openRedirect(rawTarget: String) {
        guard let target = URL(string: rawTarget) else { return }
        UIApplication.shared.open(target)
    }
}

Fixed example

import SafariServices
import UIKit

final class RedirectViewController: UIViewController {
    private let allowedHosts: Set<String> = ["app.example.com"]

    func openRedirect(rawTarget: String) {
        guard let target = URL(string: rawTarget),
              target.scheme == "https",
              let host = target.host,
              allowedHosts.contains(host) else { return }

        let browser = SFSafariViewController(url: target)
        present(browser, animated: true)
    }
}

M4

Insufficient Input/Output Validation

Insufficient Input/Output Validation means the app trusts deep links, QR values, intents, files, server responses, or rendered output without checking format, source, and bounds. Tampered data can open an unsafe target, alter a workflow, or send data to the wrong destination.

Risk at a glance
What can go wrongData crossing application, device, backend, browser, or third-party boundaries is trusted without sufficient checks.
Business effectMalicious content can alter an app flow, be rendered unsafely, or drive an unsafe backend operation.
First control to verifyValidate all external data at each trust boundary.

Attacker Victim Application Security control

M4 attack path against a victim application and its mitigating control

The diagram contrasts the unsafe route with the control that interrupts it. Select the image to inspect it at full size.

Why it matters

Malicious content can alter an app flow, be rendered unsafely, or drive an unsafe backend operation.

Risk-reduction focus

  • Validate all external data at each trust boundary.
  • Encode output for its rendering context.
  • Use typed contracts and reject unexpected fields.
Android example in Kotlin

Vulnerable example

import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity

class RedirectActivity : AppCompatActivity() {
    fun openRedirect(intent: Intent) {
        val rawTarget = intent.data?.getQueryParameter("redirect") ?: return
        startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(rawTarget)))
    }
}

Fixed example

import android.content.Intent
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import androidx.appcompat.app.AppCompatActivity

class RedirectActivity : AppCompatActivity() {
    private val allowedHosts = setOf("app.example.com")

    fun openRedirect(intent: Intent) {
        val rawTarget = intent.data?.getQueryParameter("redirect") ?: return
        val target = Uri.parse(rawTarget)
        if (target.scheme != "https" || target.host !in allowedHosts) return
        CustomTabsIntent.Builder().build().launchUrl(this, target)
    }
}
iOS example in Swift

Vulnerable example

import UIKit

final class RedirectViewController: UIViewController {
    func openRedirect(rawTarget: String) {
        guard let target = URL(string: rawTarget) else { return }
        UIApplication.shared.open(target)
    }
}

Fixed example

import SafariServices
import UIKit

final class RedirectViewController: UIViewController {
    private let allowedHosts: Set<String> = ["app.example.com"]

    func openRedirect(rawTarget: String) {
        guard let target = URL(string: rawTarget),
              target.scheme == "https",
              let host = target.host,
              allowedHosts.contains(host) else { return }

        let browser = SFSafariViewController(url: target)
        present(browser, animated: true)
    }
}

M5

Insecure Communication

Insecure Communication means app-to-service data is not sufficiently protected against interception or modification. Trusting unverified certificates, using HTTP, incomplete TLS validation, or sending secrets through another unsafe channel can let a nearby network attacker read or alter traffic.

Risk at a glance
What can go wrongNetwork traffic, certificate handling, or channel selection does not adequately protect data in transit.
Business effectA hostile or compromised network can observe, alter, or redirect sensitive exchanges.
First control to verifyRequire modern TLS for sensitive connections.

Attacker Victim Application Security control

M5 attack path against a victim application and its mitigating control

The diagram contrasts the unsafe route with the control that interrupts it. Select the image to inspect it at full size.

Why it matters

A hostile or compromised network can observe, alter, or redirect sensitive exchanges.

Risk-reduction focus

  • Require modern TLS for sensitive connections.
  • Validate certificates and avoid trust-all behavior.
  • Limit sensitive data in requests and diagnostics.
Android example in Kotlin

Vulnerable example

import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity

class RedirectActivity : AppCompatActivity() {
    fun openRedirect(intent: Intent) {
        val rawTarget = intent.data?.getQueryParameter("redirect") ?: return
        startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(rawTarget)))
    }
}

Fixed example

import android.content.Intent
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import androidx.appcompat.app.AppCompatActivity

class RedirectActivity : AppCompatActivity() {
    private val allowedHosts = setOf("app.example.com")

    fun openRedirect(intent: Intent) {
        val rawTarget = intent.data?.getQueryParameter("redirect") ?: return
        val target = Uri.parse(rawTarget)
        if (target.scheme != "https" || target.host !in allowedHosts) return
        CustomTabsIntent.Builder().build().launchUrl(this, target)
    }
}
iOS example in Swift

Vulnerable example

import UIKit

final class RedirectViewController: UIViewController {
    func openRedirect(rawTarget: String) {
        guard let target = URL(string: rawTarget) else { return }
        UIApplication.shared.open(target)
    }
}

Fixed example

import SafariServices
import UIKit

final class RedirectViewController: UIViewController {
    private let allowedHosts: Set<String> = ["app.example.com"]

    func openRedirect(rawTarget: String) {
        guard let target = URL(string: rawTarget),
              target.scheme == "https",
              let host = target.host,
              allowedHosts.contains(host) else { return }

        let browser = SFSafariViewController(url: target)
        present(browser, animated: true)
    }
}

M6

Inadequate Privacy Controls

Inadequate Privacy Controls occur when an app collects, uses, discloses, or retains personal data beyond a clear purpose or without understandable control. Unneeded permissions, identifiable analytics, or missing consent management create risks for users, trust, and privacy obligations.

Risk at a glance
What can go wrongThe app collects, shares, retains, or exposes personal data beyond the disclosed and necessary purpose.
Business effectUsers can lose control of personal information through telemetry, permissions, analytics, or third-party sharing.
First control to verifyMap personal-data collection and recipients.

Attacker Victim Application Security control

M6 attack path against a victim application and its mitigating control

The diagram contrasts the unsafe route with the control that interrupts it. Select the image to inspect it at full size.

Why it matters

Users can lose control of personal information through telemetry, permissions, analytics, or third-party sharing.

Risk-reduction focus

  • Map personal-data collection and recipients.
  • Request only necessary permissions at the right time.
  • Apply retention, consent, and deletion controls.
Android example in Kotlin

Vulnerable example

import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity

class RedirectActivity : AppCompatActivity() {
    fun openRedirect(intent: Intent) {
        val rawTarget = intent.data?.getQueryParameter("redirect") ?: return
        startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(rawTarget)))
    }
}

Fixed example

import android.content.Intent
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import androidx.appcompat.app.AppCompatActivity

class RedirectActivity : AppCompatActivity() {
    private val allowedHosts = setOf("app.example.com")

    fun openRedirect(intent: Intent) {
        val rawTarget = intent.data?.getQueryParameter("redirect") ?: return
        val target = Uri.parse(rawTarget)
        if (target.scheme != "https" || target.host !in allowedHosts) return
        CustomTabsIntent.Builder().build().launchUrl(this, target)
    }
}
iOS example in Swift

Vulnerable example

import UIKit

final class RedirectViewController: UIViewController {
    func openRedirect(rawTarget: String) {
        guard let target = URL(string: rawTarget) else { return }
        UIApplication.shared.open(target)
    }
}

Fixed example

import SafariServices
import UIKit

final class RedirectViewController: UIViewController {
    private let allowedHosts: Set<String> = ["app.example.com"]

    func openRedirect(rawTarget: String) {
        guard let target = URL(string: rawTarget),
              target.scheme == "https",
              let host = target.host,
              allowedHosts.contains(host) else { return }

        let browser = SFSafariViewController(url: target)
        present(browser, animated: true)
    }
}

M7

Insufficient Binary Protections

Insufficient Binary Protections mean an app binary is easy to inspect, modify, or repackage so client-side safeguards can be bypassed. Missing tamper detection or device-integrity checks are examples. These controls do not replace server-side enforcement, but they raise attack cost and create detection signals.

Risk at a glance
What can go wrongThe distributed app does not sufficiently resist unauthorized modification, repackaging, or analysis for its threat model.
Business effectA modified client can remove controls, expose implementation details, or misrepresent a trusted application to users.
First control to verifyKeep trust decisions and secrets server-side.

Attacker Victim Application Security control

M7 attack path against a victim application and its mitigating control

The diagram contrasts the unsafe route with the control that interrupts it. Select the image to inspect it at full size.

Why it matters

A modified client can remove controls, expose implementation details, or misrepresent a trusted application to users.

Risk-reduction focus

  • Keep trust decisions and secrets server-side.
  • Use platform signing and integrity signals where appropriate.
  • Detect tampering proportionately and plan safe responses.
Android example in Kotlin

Vulnerable example

import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity

class RedirectActivity : AppCompatActivity() {
    fun openRedirect(intent: Intent) {
        val rawTarget = intent.data?.getQueryParameter("redirect") ?: return
        startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(rawTarget)))
    }
}

Fixed example

import android.content.Intent
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import androidx.appcompat.app.AppCompatActivity

class RedirectActivity : AppCompatActivity() {
    private val allowedHosts = setOf("app.example.com")

    fun openRedirect(intent: Intent) {
        val rawTarget = intent.data?.getQueryParameter("redirect") ?: return
        val target = Uri.parse(rawTarget)
        if (target.scheme != "https" || target.host !in allowedHosts) return
        CustomTabsIntent.Builder().build().launchUrl(this, target)
    }
}
iOS example in Swift

Vulnerable example

import UIKit

final class RedirectViewController: UIViewController {
    func openRedirect(rawTarget: String) {
        guard let target = URL(string: rawTarget) else { return }
        UIApplication.shared.open(target)
    }
}

Fixed example

import SafariServices
import UIKit

final class RedirectViewController: UIViewController {
    private let allowedHosts: Set<String> = ["app.example.com"]

    func openRedirect(rawTarget: String) {
        guard let target = URL(string: rawTarget),
              target.scheme == "https",
              let host = target.host,
              allowedHosts.contains(host) else { return }

        let browser = SFSafariViewController(url: target)
        present(browser, animated: true)
    }
}

M8

Security Misconfiguration

Mobile Security Misconfiguration is unnecessary exposure through capabilities, components, debug settings, permissions, or network policy. An exported activity, extractable backup, or release debug endpoint can let another app invoke functionality or access data outside the intended UI.

Risk at a glance
What can go wrongBuild variants, platform settings, exported components, or backend configuration expose unintended behavior.
Business effectDebug features, exported components, permissive backups, or weak transport settings can be reachable in a release.
First control to verifyReview release manifests and platform settings.

Attacker Victim Application Security control

M8 attack path against a victim application and its mitigating control

The diagram contrasts the unsafe route with the control that interrupts it. Select the image to inspect it at full size.

Why it matters

Debug features, exported components, permissive backups, or weak transport settings can be reachable in a release.

Risk-reduction focus

  • Review release manifests and platform settings.
  • Separate debug and production configuration.
  • Automate configuration checks in CI.
Android example in Kotlin

Vulnerable example

import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity

class RedirectActivity : AppCompatActivity() {
    fun openRedirect(intent: Intent) {
        val rawTarget = intent.data?.getQueryParameter("redirect") ?: return
        startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(rawTarget)))
    }
}

Fixed example

import android.content.Intent
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import androidx.appcompat.app.AppCompatActivity

class RedirectActivity : AppCompatActivity() {
    private val allowedHosts = setOf("app.example.com")

    fun openRedirect(intent: Intent) {
        val rawTarget = intent.data?.getQueryParameter("redirect") ?: return
        val target = Uri.parse(rawTarget)
        if (target.scheme != "https" || target.host !in allowedHosts) return
        CustomTabsIntent.Builder().build().launchUrl(this, target)
    }
}
iOS example in Swift

Vulnerable example

import UIKit

final class RedirectViewController: UIViewController {
    func openRedirect(rawTarget: String) {
        guard let target = URL(string: rawTarget) else { return }
        UIApplication.shared.open(target)
    }
}

Fixed example

import SafariServices
import UIKit

final class RedirectViewController: UIViewController {
    private let allowedHosts: Set<String> = ["app.example.com"]

    func openRedirect(rawTarget: String) {
        guard let target = URL(string: rawTarget),
              target.scheme == "https",
              let host = target.host,
              allowedHosts.contains(host) else { return }

        let browser = SFSafariViewController(url: target)
        present(browser, animated: true)
    }
}

M9

Insecure Data Storage

Insecure Data Storage means sensitive data is saved on the device without suitable protection, such as in shared preferences, caches, logs, screenshots, clipboard, or backups. It can leak through rooted or jailbroken devices, backup extraction, malware, or temporary physical access.

Risk at a glance
What can go wrongSensitive data is stored locally or cached where another application, backup, device user, or forensic process can access it.
Business effectA lost, rooted, shared, or backed-up device can reveal account, business, or personal data.
First control to verifyMinimize local sensitive data and cache duration.

Attacker Victim Application Security control

M9 attack path against a victim application and its mitigating control

The diagram contrasts the unsafe route with the control that interrupts it. Select the image to inspect it at full size.

Why it matters

A lost, rooted, shared, or backed-up device can reveal account, business, or personal data.

Risk-reduction focus

  • Minimize local sensitive data and cache duration.
  • Use platform-protected storage for necessary secrets.
  • Review logs, screenshots, backups, and shared storage.
Android example in Kotlin

Vulnerable example

import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity

class RedirectActivity : AppCompatActivity() {
    fun openRedirect(intent: Intent) {
        val rawTarget = intent.data?.getQueryParameter("redirect") ?: return
        startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(rawTarget)))
    }
}

Fixed example

import android.content.Intent
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import androidx.appcompat.app.AppCompatActivity

class RedirectActivity : AppCompatActivity() {
    private val allowedHosts = setOf("app.example.com")

    fun openRedirect(intent: Intent) {
        val rawTarget = intent.data?.getQueryParameter("redirect") ?: return
        val target = Uri.parse(rawTarget)
        if (target.scheme != "https" || target.host !in allowedHosts) return
        CustomTabsIntent.Builder().build().launchUrl(this, target)
    }
}
iOS example in Swift

Vulnerable example

import UIKit

final class RedirectViewController: UIViewController {
    func openRedirect(rawTarget: String) {
        guard let target = URL(string: rawTarget) else { return }
        UIApplication.shared.open(target)
    }
}

Fixed example

import SafariServices
import UIKit

final class RedirectViewController: UIViewController {
    private let allowedHosts: Set<String> = ["app.example.com"]

    func openRedirect(rawTarget: String) {
        guard let target = URL(string: rawTarget),
              target.scheme == "https",
              let host = target.host,
              allowedHosts.contains(host) else { return }

        let browser = SFSafariViewController(url: target)
        present(browser, animated: true)
    }
}

M10

Insufficient Cryptography

Insufficient Cryptography means an app selects or applies algorithms, keys, random values, or libraries incorrectly even when encryption exists. Guessable keys, unsuitable modes, or readable key storage can let an attacker who obtains data or keys decrypt or forge it.

Risk at a glance
What can go wrongCryptographic algorithms, modes, random values, keys, or implementations do not protect mobile data as intended.
Business effectData protected by weak or incorrectly implemented cryptography can be recovered or altered.
First control to verifyUse platform and vetted cryptographic APIs.

Attacker Victim Application Security control

M10 attack path against a victim application and its mitigating control

The diagram contrasts the unsafe route with the control that interrupts it. Select the image to inspect it at full size.

Why it matters

Data protected by weak or incorrectly implemented cryptography can be recovered or altered.

Risk-reduction focus

  • Use platform and vetted cryptographic APIs.
  • Generate and store keys with platform protections.
  • Avoid custom cryptography and obsolete algorithms.
Android example in Kotlin

Vulnerable example

import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity

class RedirectActivity : AppCompatActivity() {
    fun openRedirect(intent: Intent) {
        val rawTarget = intent.data?.getQueryParameter("redirect") ?: return
        startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(rawTarget)))
    }
}

Fixed example

import android.content.Intent
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import androidx.appcompat.app.AppCompatActivity

class RedirectActivity : AppCompatActivity() {
    private val allowedHosts = setOf("app.example.com")

    fun openRedirect(intent: Intent) {
        val rawTarget = intent.data?.getQueryParameter("redirect") ?: return
        val target = Uri.parse(rawTarget)
        if (target.scheme != "https" || target.host !in allowedHosts) return
        CustomTabsIntent.Builder().build().launchUrl(this, target)
    }
}
iOS example in Swift

Vulnerable example

import UIKit

final class RedirectViewController: UIViewController {
    func openRedirect(rawTarget: String) {
        guard let target = URL(string: rawTarget) else { return }
        UIApplication.shared.open(target)
    }
}

Fixed example

import SafariServices
import UIKit

final class RedirectViewController: UIViewController {
    private let allowedHosts: Set<String> = ["app.example.com"]

    func openRedirect(rawTarget: String) {
        guard let target = URL(string: rawTarget),
              target.scheme == "https",
              let host = target.host,
              allowedHosts.contains(host) else { return }

        let browser = SFSafariViewController(url: target)
        present(browser, animated: true)
    }
}

Official OWASP reference

Open the official OWASP edition