How We Audited a High-Traffic Shopify Enterprise Stack on Linode and Mitigated Cross-Site Scripting (XSS) in custom themes
Auditing the Linode Enterprise Stack
Our engagement began with a deep dive into a high-traffic Shopify enterprise deployment hosted on Linode. The primary objective was to identify and remediate critical security vulnerabilities, with a specific focus on Cross-Site Scripting (XSS) within custom theme code. This wasn’t a typical pentest; it required understanding the interplay between Shopify’s platform, custom Liquid templating, JavaScript execution, and the underlying Linode infrastructure.
The stack comprised several Linode instances, including dedicated web servers, a managed PostgreSQL database, and Redis for caching. Traffic was managed by HAProxy, with Nginx acting as a reverse proxy and serving static assets. The core of the e-commerce functionality was driven by a heavily customized Shopify theme, which was the initial suspect for potential XSS vectors.
Initial Reconnaissance and Attack Surface Mapping
Our first step was to map the attack surface. This involved:
- Network Topology Analysis: Understanding the Linode VPC configuration, firewall rules (iptables/ufw on instances, Linode Cloud Firewall), and HAProxy/Nginx ingress points.
- Application Entry Points: Identifying all user-controllable input fields, URL parameters, HTTP headers, and cookies that could be manipulated. This included standard Shopify checkout flows, customer account pages, search functionalities, and importantly, any custom forms or widgets within the theme.
- Third-Party Integrations: Cataloging all external JavaScript libraries, APIs, and services integrated into the Shopify store. These are often overlooked but can be significant sources of vulnerability.
- Custom Code Review (Static Analysis): A preliminary scan of the theme’s Liquid, JavaScript, and CSS files for obvious patterns indicative of XSS (e.g., direct `innerHTML` assignments without sanitization, unescaped user input in script tags).
Deep Dive: XSS in Custom Shopify Themes
Shopify themes are built using Liquid, HTML, CSS, and JavaScript. While Liquid itself is server-side and generally safe from client-side XSS, the way it renders dynamic content into HTML and the subsequent JavaScript execution are prime targets. We focused on two main areas:
1. Unsanitized Data Rendering in HTML: Liquid’s `{{ variable }}` syntax automatically escapes HTML by default. However, the `| raw` filter bypasses this. We searched for instances where user-controlled data was passed through this filter or directly embedded into HTML attributes without proper sanitization.
Example: Vulnerable Liquid Rendering
Consider a custom product review widget where a user’s name or comment might be displayed. A naive implementation could look like this:
Vulnerable Code Snippet (theme.liquid or a snippet file):
Imagine a scenario where a product review submission form allows users to input their name and review text. This data, if stored and later displayed without sanitization, could lead to XSS.
Hypothetical Vulnerable Liquid Snippet:
<div class="review">
<h4>Review by {{ review.author_name | raw }}</h4> <!-- Potential XSS here -->
<p>{{ review.content }}</p>
</div>
If `review.author_name` could be influenced by an attacker and rendered using `| raw`, an attacker could inject malicious JavaScript. For instance, if `review.author_name` was set to <script>alert('XSS')</script>, and the `| raw` filter was applied, the script would execute.
Mitigation: Proper Escaping and Sanitization
The fix is to ensure that any dynamic content, especially if it originates from user input or external sources, is properly escaped. For Liquid, this means avoiding the `| raw` filter unless absolutely necessary and validated. If HTML is truly required, a robust sanitization library should be employed on the server-side (if possible) or client-side before rendering.
Secure Liquid Snippet:
<div class="review">
<h4>Review by {{ review.author_name }}</h4> <!-- Default escaping handles this -->
<p>{{ review.content }}</p>
</div>
If `review.author_name` contained HTML tags, they would be rendered as text (e.g., <script>alert('XSS')</script>), preventing script execution.
2. Client-Side JavaScript Vulnerabilities
Custom themes often include significant amounts of JavaScript to enhance user experience. These scripts can be vulnerable if they:
- Directly use user-controlled input (from URL parameters, form fields, `localStorage`, `sessionStorage`, etc.) to manipulate the DOM via methods like `innerHTML`, `outerHTML`, `document.write()`, or by constructing script URLs (`javascript:`).
- Fail to properly sanitize data before passing it to `eval()` or similar unsafe functions.
- Are susceptible to DOM-based XSS where the vulnerability lies entirely within the client-side script’s logic.
Example: Vulnerable JavaScript DOM Manipulation
Consider a JavaScript function that takes a search query from the URL and displays it dynamically on the page.
Vulnerable JavaScript Snippet (in a theme’s .js file):
function displaySearchQuery() {
const urlParams = new URLSearchParams(window.location.search);
const query = urlParams.get('q'); // Get query parameter
if (query) {
const displayArea = document.getElementById('search-query-display');
if (displayArea) {
// Vulnerable: Directly injecting HTML from user-controlled input
displayArea.innerHTML = `You searched for: <strong>${query}</strong>`;
}
}
}
// Assume this is called on page load
// displaySearchQuery();
An attacker could craft a URL like: https://your-store.com/search?q=<img src=x onerror=alert('XSS')>. When `displaySearchQuery` executes, the `innerHTML` assignment would render the malicious image tag, triggering the `onerror` event and executing the JavaScript payload.
Mitigation: DOMPurify and Safe DOM Manipulation
The most robust solution for client-side sanitization is using a well-vetted library like DOMPurify. It’s designed to sanitize HTML and make it safe to use within JavaScript. Alternatively, avoid `innerHTML` altogether and use safer methods like `textContent` or `innerText` when displaying plain text, or carefully construct DOM elements programmatically.
Secure JavaScript Snippet using DOMPurify:
// Ensure DOMPurify is included in your theme assets
// e.g., <script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.3.3/purify.min.js"></script>
function displaySearchQuerySafe() {
const urlParams = new URLSearchParams(window.location.search);
const query = urlParams.get('q');
if (query) {
const displayArea = document.getElementById('search-query-display');
if (displayArea) {
// Sanitize the input before setting innerHTML
const sanitizedQuery = DOMPurify.sanitize(query);
displayArea.innerHTML = `You searched for: <strong>${sanitizedQuery}</strong>`;
}
}
}
// displaySearchQuerySafe();
If the attacker crafts the same malicious URL, `DOMPurify.sanitize(query)` would strip out the dangerous parts, rendering the input safely. For example, <img src=x onerror=alert('XSS')> would become <img src="x"> (or similar, depending on DOMPurify’s exact configuration and context), preventing execution.
Infrastructure-Level Security Considerations
While the primary focus was the application layer, we also reviewed the Linode infrastructure configuration:
- Firewall Rules: Ensured that only necessary ports were open on the Linode instances and that Linode Cloud Firewall rules were restrictive. For example, web servers should only accept traffic on ports 80 and 443, and database servers should only be accessible from specific application servers.
- HAProxy Configuration: Verified that HAProxy was correctly configured for SSL termination, load balancing, and potentially rate limiting to mitigate DoS attacks. We checked for common misconfigurations that could expose backend servers.
- Nginx Configuration: Ensured Nginx was hardened, with unnecessary modules disabled, proper logging, and security headers (like `Content-Security-Policy`, `X-Frame-Options`, `X-Content-Type-Options`) being served.
- SSH Access: Confirmed that SSH access was restricted to specific IP addresses, used key-based authentication, and had strong password policies (if applicable).
- Regular Patching: Assessed the process for keeping the Linode operating systems and any installed software (like Nginx, HAProxy, Redis) up-to-date with security patches.
Example: Nginx Security Headers
Implementing strong security headers via Nginx is a crucial defense-in-depth measure against XSS and other injection attacks.
# In your Nginx server block configuration add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; # Content-Security-Policy is powerful but complex. Start simple and iterate. # This example is restrictive and might break some functionalities if not carefully tuned. add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.shopify.com https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://cdn.shopify.com; font-src 'self' data:; connect-src 'self' https://your-store.myshopify.com; frame-ancestors 'self' https://your-store.myshopify.com;" always;
The Content-Security-Policy (CSP) header is particularly effective. It instructs the browser on which dynamic resources (scripts, styles, images, etc.) are allowed to load. By default, it should be set to 'self', meaning only resources from the same origin are permitted. 'unsafe-inline' is often required for existing JavaScript, but the goal should be to remove it over time. For Shopify, you’ll need to explicitly allow resources from cdn.shopify.com and potentially other trusted CDNs.
Automated Scanning and Manual Validation
We employed a combination of automated tools and manual testing:
- Static Application Security Testing (SAST): Tools like
eslintwith security plugins, or more advanced SAST solutions, were used to scan the theme’s JavaScript and Liquid files for known vulnerable patterns. - Dynamic Application Security Testing (DAST): Tools like OWASP ZAP or Burp Suite were configured to crawl the Shopify store, focusing on user-input fields and API endpoints. These tools helped identify reflected and stored XSS vulnerabilities.
- Manual Code Review: This remained critical. Automated tools can miss context-specific vulnerabilities. A manual review of the identified suspicious code patterns and the application’s business logic was essential for confirmation and understanding the full impact.
- Browser Developer Tools: Extensive use of browser developer consoles to inspect DOM manipulation, network requests, and JavaScript execution flow.
Remediation and Verification
Once vulnerabilities were identified and confirmed, we worked with the development team to:
- Implement Fixes: Apply the sanitization and escaping techniques discussed above to the theme code. This often involved refactoring JavaScript or adjusting Liquid logic.
- Update Nginx/HAProxy Configs: Deploy necessary security header changes and firewall rule adjustments.
- Re-testing: After remediation, we re-tested all identified vulnerabilities using the same tools and manual techniques to ensure they were effectively mitigated and that no new issues were introduced.
- Documentation: Provided a detailed report outlining the vulnerabilities, their impact, the remediation steps taken, and recommendations for ongoing security practices.
Auditing a high-traffic e-commerce platform requires a layered approach, combining application-level security with robust infrastructure hardening. For Shopify, the custom theme code presents a unique challenge, demanding careful attention to how dynamic data is handled within Liquid and JavaScript.