How We Audited a High-Traffic Shopify Enterprise Stack on OVH and Mitigated access token leakages via unvalidated application redirections
Auditing the OVH-Hosted Shopify Enterprise Stack
Our engagement began with a critical security audit of a high-traffic Shopify enterprise deployment hosted on OVH infrastructure. The primary concern was the potential for sensitive data exfiltration, particularly access tokens, due to misconfigurations or vulnerabilities within the custom application layer and its integration points with Shopify. The stack comprised a complex web of microservices, a robust API gateway, and several third-party integrations, all orchestrated within OVH’s dedicated server environment.
The initial phase involved a deep dive into the network topology and access control mechanisms. We focused on identifying any publicly accessible endpoints that were not strictly necessary for core functionality and scrutinizing the ingress/egress rules for all services. A key area of investigation was the application’s handling of OAuth 2.0 flows, specifically the redirection mechanisms post-authentication.
Identifying Unvalidated Redirect Vulnerabilities
A common, yet often overlooked, vulnerability in OAuth 2.0 implementations is the “open redirect” or “unvalidated redirection” flaw. This occurs when an application redirects a user to a URL that is not properly validated against a trusted list of redirect URIs. In a Shopify context, this can be particularly dangerous if an attacker can trick a user into clicking a malicious link that redirects them through the compromised application, potentially stealing their Shopify access tokens or session cookies.
We began by examining the application’s authentication and authorization code, specifically looking for parameters like `redirect_uri` or similar fields that control where a user is sent after a successful login or authorization. The goal was to find instances where these parameters were directly incorporated into outgoing HTTP redirects without strict validation.
Consider a hypothetical PHP endpoint responsible for handling the OAuth callback:
<?php
// Hypothetical callback handler
session_start();
// Assume $shopify_client is an authenticated Shopify API client instance
if (isset($_GET['code'])) {
$auth_code = $_GET['code'];
$redirect_url_from_param = $_GET['redirect_uri'] ?? '/dashboard'; // Default redirect
try {
// Exchange authorization code for access token
$token_response = $shopify_client->getAccessToken($auth_code);
$_SESSION['shopify_access_token'] = $token_response['access_token'];
$_SESSION['shop_domain'] = $_SESSION['shop_domain']; // Assuming shop domain is already set
// **VULNERABLE REDIRECTION POINT**
// The $redirect_url_from_param is used directly without validation.
header("Location: " . $redirect_url_from_param);
exit();
} catch (Exception $e) {
// Log error and redirect to an error page
error_log("OAuth Error: " . $e->getMessage());
header("Location: /error?message=" . urlencode("Authentication failed."));
exit();
}
} else {
// Handle missing code parameter
header("Location: /error?message=" . urlencode("Missing authorization code."));
exit();
}
?>
In this snippet, the `redirect_url_from_param` is taken directly from the `$_GET[‘redirect_uri’]` parameter. If this parameter is not validated against a predefined list of allowed redirect URIs, an attacker could craft a malicious URL. For instance, they could provide a `redirect_uri` pointing to an attacker-controlled domain, or even to a sensitive internal endpoint if network segmentation is weak.
Exploitation Scenario: Access Token Leakage
The critical risk here is that the access token, once obtained by the application, might be inadvertently exposed. If the unvalidated redirect leads to a page that logs query parameters, or if the attacker can manipulate the redirect to a page that reflects user input, the access token could be leaked. A more sophisticated attack could involve redirecting to a specially crafted URL on a domain controlled by the attacker, which then captures the token from the URL fragment or query string.
Consider an attacker crafting a malicious link:
https://your-shopify-app.com/callback.php?code=AUTH_CODE_HERE&redirect_uri=https://attacker.com/log_token?token=
If the application’s callback handler is vulnerable as shown above, and the `redirect_uri` parameter is not validated, the user’s browser would be redirected to `https://attacker.com/log_token?token=AUTH_CODE_HERE`. If the `AUTH_CODE_HERE` part is actually the *access token* (which can happen if the flow is misconfigured or if the attacker can manipulate the `code` parameter to be something else that gets reflected), or if the attacker can chain this with another vulnerability to get the token, they could steal it. A more direct attack would be to craft a redirect to a page on the attacker’s domain that *also* includes a legitimate redirect URI, but in a way that the application’s response includes the token.
A more realistic scenario involves the attacker tricking a legitimate user into clicking a link that initiates the OAuth flow. The attacker would pre-register their malicious domain as a potential redirect URI (if the application allows dynamic registration or has a weak validation mechanism). When the user authorizes the app, the attacker’s domain receives the authorization code. The attacker’s server then exchanges this code for an access token and immediately redirects the user to a legitimate page on the compromised Shopify store or application, but the attacker has already captured the token.
Mitigation Strategy: Strict Redirect URI Validation
The most effective mitigation is to implement strict validation of all redirect URIs. This involves maintaining a whitelist of all allowed redirect URIs and ensuring that any URI provided in the `redirect_uri` parameter during the OAuth flow exactly matches one of the entries in this whitelist. This validation should occur server-side, before any redirection takes place.
Here’s how we refactored the PHP callback handler to include this crucial validation:
<?php
// Refactored callback handler with validation
session_start();
// Define the whitelist of allowed redirect URIs
$allowed_redirect_uris = [
'https://your-shopify-app.com/dashboard',
'https://your-shopify-app.com/settings',
'https://your-shopify-app.com/oauth/success',
// Add other legitimate redirect URIs here
];
// Assume $shopify_client is an authenticated Shopify API client instance
if (isset($_GET['code'])) {
$auth_code = $_GET['code'];
$requested_redirect_url = $_GET['redirect_uri'] ?? '/dashboard'; // Default if not provided
// **STRICT VALIDATION**
// Normalize the requested URL to prevent bypasses (e.g., trailing slashes, case sensitivity)
$normalized_requested_url = rtrim($requested_redirect_url, '/');
$is_valid_redirect = false;
foreach ($allowed_redirect_uris as $allowed_uri) {
if (rtrim($allowed_uri, '/') === $normalized_requested_url) {
$is_valid_redirect = true;
break;
}
}
if (!$is_valid_redirect) {
error_log("Invalid redirect URI attempted: " . $requested_redirect_url);
header("Location: /error?message=" . urlencode("Invalid redirect URI."));
exit();
}
try {
// Exchange authorization code for access token
$token_response = $shopify_client->getAccessToken($auth_code);
$_SESSION['shopify_access_token'] = $token_response['access_token'];
$_SESSION['shop_domain'] = $_SESSION['shop_domain']; // Assuming shop domain is already set
// Redirect to the validated URL
header("Location: " . $requested_redirect_url);
exit();
} catch (Exception $e) {
// Log error and redirect to an error page
error_log("OAuth Error: " . $e->getMessage());
header("Location: /error?message=" . urlencode("Authentication failed."));
exit();
}
} else {
// Handle missing code parameter
header("Location: /error?message=" . urlencode("Missing authorization code."));
exit();
}
?>
In this improved version:
- A `$allowed_redirect_uris` array is defined, containing all legitimate destinations for redirects.
- The incoming `$_GET[‘redirect_uri’]` is normalized (e.g., by removing trailing slashes) to ensure consistent comparison.
- The normalized requested URL is iterated against the whitelist. If no match is found, an error is logged, and the user is redirected to a generic error page, preventing the malicious redirect.
- Only if the redirect URI is validated is the OAuth token exchange performed, and then the user is redirected to the *validated* URI.
OVH Infrastructure Configuration Review
Beyond application-level fixes, we also reviewed the OVH infrastructure configuration. This included:
- Firewall Rules (OVH Control Panel): Ensuring that only necessary ports are open to the internet and that ingress traffic is restricted to known IP ranges where applicable. We verified that no sensitive management ports were exposed unnecessarily.
- Web Server Configuration (Nginx/Apache): For any static assets or API gateway endpoints served directly by Nginx or Apache, we checked for misconfigurations that could lead to information disclosure. This included reviewing access logs for suspicious patterns and ensuring proper `Content-Security-Policy` headers were set.
- Network Segmentation: Confirming that different microservices or components of the stack were not overly exposed to each other. For instance, a database server should not be directly accessible from the public internet, nor should internal services be accessible from less trusted network segments.
- SSL/TLS Configuration: Verifying that all traffic was encrypted using strong TLS versions and cipher suites, and that certificates were up-to-date and properly configured.
For example, a typical Nginx configuration snippet for an API gateway might look like this, with a focus on security headers:
server {
listen 443 ssl http2;
server_name api.your-shopify-app.com;
ssl_certificate /etc/letsencrypt/live/api.your-shopify-app.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.your-shopify-app.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Content-Security-Policy can be complex and application-specific,
# but a basic example to prevent XSS via script injection:
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; object-src 'none'; style-src 'self' 'unsafe-inline';" always;
location / {
proxy_pass http://your_backend_service;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# ... other configurations for rate limiting, access control, etc.
}
This Nginx configuration enforces strong TLS, sets crucial security headers to protect against common web attacks like clickjacking and XSS, and correctly proxies requests to the backend service while preserving important request information.
Conclusion and Ongoing Monitoring
The audit successfully identified and mitigated a critical unvalidated redirect vulnerability that could have led to access token leakage. The remediation involved both application code changes (strict redirect URI validation) and infrastructure hardening on OVH. It’s crucial to remember that security is an ongoing process. We recommended implementing continuous monitoring for suspicious access patterns, regular security audits, and a robust incident response plan to quickly address any future security events.