{"id":50035,"date":"2026-11-12T19:16:03","date_gmt":"2026-11-12T13:46:03","guid":{"rendered":"https:\/\/www.getastra.com\/blog\/?p=50035"},"modified":"2026-11-12T19:16:10","modified_gmt":"2026-11-12T13:46:10","slug":"shopify-app-security-checklist","status":"publish","type":"post","link":"https:\/\/www.getastra.com\/blog\/security-audit\/shopify-app-security-checklist\/","title":{"rendered":"Shopify App Security Checklist: What to Test Before App Store Review"},"content":{"rendered":"<div class=\"gb-container gb-container-e43a8917\">\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Key_Takeaways\"><\/span>Key Takeaways<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Ensure one Shopify store cannot access another store\u2019s data, even by manipulating IDs or requests.<\/li>\n\n\n\n<li>Validate OAuth, session tokens, webhooks, app proxies and API requests to prevent unauthorised access as part of the Shopify app security checklist.<\/li>\n\n\n\n<li>Don&#8217;t rely on automated checks alone; Shopify&#8217;s AI and code scans can&#8217;t fully test live app behaviour, cross-store access or business logic vulnerabilities.<\/li>\n\n\n\n<li>Get an independent security assessment with Astra&#8217;s certified pentesters to test business logic flaws, provide remediation guidance and offer free rescans to verify fixes.<\/li>\n<\/ul>\n\n<\/div>\n\n\n<p class=\"wp-block-paragraph\">Most teams treat Shopify app review as a formality until it isn\u2019t. In February 2026, Shopify\u2019s developer team told the community that review times were longer than usual, and that resubmitting sends you to the back of the queue.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Moreover, it\u2019s easy enough to catch a failed install on your own. What\u2019s easier to miss is what happens once a second store is using your app. Most Shopify apps serve many stores; as Shopify\u2019s own security policy notes, bugs that matter include cases where one store can reach another store\u2019s data.&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This checklist covers what to test before you submit, one check at a time, each with a pass\/fail test. Every section ends with a Second Store Test, run from a second dev store. It\u2019s independent, built from Shopify\u2019s public documentation, and not an official Shopify list. Short on time? Jump to <a href=\"#review-timelines\">review timelines<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Why_Do_Shopify_Apps_Get_Rejected_on_Security\"><\/span>Why Do Shopify Apps Get Rejected on Security?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Most security-related rejections show up as functional failures: broken install, OAuth, embedding or webhook handling. Shopify\u2019s <a href=\"https:\/\/shopify.dev\/docs\/apps\/launch\/app-store-review\/pass-app-review\" target=\"_blank\" rel=\"noopener\">common review problems<\/a> include:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>No redirect to the OAuth grant screen after install<\/li>\n\n\n\n<li>A fatal error after install, or an app that won\u2019t reinstall<\/li>\n\n\n\n<li>Switching between embedded and non-embedded versions<\/li>\n\n\n\n<li>Broken UI, or 404, 500 and 300-series errors<\/li>\n\n\n\n<li>Missing testing instructions or credentials<\/li>\n\n\n\n<li>Missing compliance webhooks and insecure webhook URLs<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Separately, Shopify\u2019s security checks flag missing mandatory GDPR compliance webhooks (<code>MISSING_COMPLIANCE_WEBHOOKS<\/code>) and non-HTTPS webhook URLs (<code>INSECURE_WEBHOOK_URL<\/code>). Security flaws such as missing authorisation or committed secrets are separate checks in Shopify\u2019s <a href=\"https:\/\/shopify.dev\/docs\/apps\/build\/security\/app-security-checks\" target=\"_blank\" rel=\"noopener\">app security checks<\/a>. The sections below cover both kinds.<\/p>\n\n\n<div class=\"gb-container gb-container-7a2ada16\">\n\n<p class=\"wp-block-paragraph\"><strong>Pro Tip:<\/strong> Install, uninstall and reinstall on two development stores, then repeat with a billing upgrade and downgrade. Shopify tells you to test on a development store before submitting.<\/p>\n\n<\/div>\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"How_Do_You_Secure_OAuth_and_Session_Authentication_in_a_Shopify_App\"><\/span>How Do You Secure OAuth and Session Authentication in a Shopify App?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Start OAuth right after install, validate every session token, and derive the shop from the authenticated session, never from request input. Some key checks include:<\/p>\n\n\n\n<div id=\"tablepress-505-scroll-wrapper\" class=\"tablepress-scroll-wrapper\">\n<table id=\"tablepress-505\" class=\"tablepress tablepress-id-505 column1-color tablepress-responsive\">\n<thead>\n<tr class=\"row-1\">\n\t<th class=\"column-1\">Check<\/th><th class=\"column-2\">How to test it<\/th>\n<\/tr>\n<\/thead>\n<tbody class=\"row-striping row-hover\">\n<tr class=\"row-2\">\n\t<td class=\"column-1\">OAuth starts immediately, and the grant screen appears<\/td><td class=\"column-2\">Install on a clean development store<\/td>\n<\/tr>\n<tr class=\"row-3\">\n\t<td class=\"column-1\">App URL and allowed redirection URLs are HTTPS &amp; match the configured values exactly; do not use wildcard redirect URLs<\/td><td class=\"column-2\">Review app config; try an altered redirect<\/td>\n<\/tr>\n<tr class=\"row-4\">\n\t<td class=\"column-1\">OAuth state is generated per attempt and validated on callback<\/td><td class=\"column-2\">Replay the callback with a wrong state<\/td>\n<\/tr>\n<tr class=\"row-5\">\n\t<td class=\"column-1\">No open redirects (OPEN_REDIRECT)<\/td><td class=\"column-2\">Pass an external URL in the redirect parameter<\/td>\n<\/tr>\n<tr class=\"row-6\">\n\t<td class=\"column-1\">Session tokens are validated on every privileged request, including signature, expiry, and the intended shop\/destination context<\/td><td class=\"column-2\">Replay an expired or altered token<\/td>\n<\/tr>\n<tr class=\"row-7\">\n\t<td class=\"column-1\">Embedded apps use session-token authentication and should not rely on third-party cookies for authentication<\/td><td class=\"column-2\">Load the app with third-party cookies blocked<\/td>\n<\/tr>\n<tr class=\"row-8\">\n\t<td class=\"column-1\">No route, job, export, GraphQL resolver or webhook handler trusts shop, customer ID or order ID from request input (REQUEST_DERIVED_SHOP_SCOPE)<\/td><td class=\"column-2\">Search handlers; confirm shop context comes from the session<\/td>\n<\/tr>\n<tr class=\"row-9\">\n\t<td class=\"column-1\">Shop-domain validation is anchored and rejects lookalike or suffixed domains (WEAK_SHOP_VALIDATION)<\/td><td class=\"column-2\">Try store.myshopify.com.evil.com; it must not validate as a legitimate Shopify shop domain.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<!-- #tablepress-505 from cache -->\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Second Store Test:<\/strong> Authenticate as Store A, then change <code>shop, host <\/code>and resource IDs to Store B\u2019s on every route. Every request should be denied. Shopify\u2019s documentation says authorization and tenant-isolation issues often can\u2019t be found by a code scan alone, so ensure you test them manually.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"How_Do_You_Set_Frame-ancestors_CSP_for_an_Embedded_Shopify_App\"><\/span>How Do You Set Frame-ancestors CSP for an Embedded Shopify App?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Return a <code>Content-Security-Policy: frame-ancestors <\/code>header that is generated per shop and includes the shop domain and <code>https:\/\/admin.shopify.com<\/code>. A static wildcard usually fails.\u00a0<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>Content-Security-Policy: frame-ancestors https:\/\/{shop-domain}.myshopify.com <a href=\"https:\/\/admin.shopify.com\" target=\"_blank\" rel=\"noopener\">https:\/\/admin.shopify.com<\/a>;<\/code><\/p>\n\n\n\n<div id=\"tablepress-506-scroll-wrapper\" class=\"tablepress-scroll-wrapper\">\n<table id=\"tablepress-506\" class=\"tablepress tablepress-id-506 column1-color tablepress-responsive\">\n<thead>\n<tr class=\"row-1\">\n\t<th class=\"column-1\">Check<\/th><th class=\"column-2\">How to test it<\/th>\n<\/tr>\n<\/thead>\n<tbody class=\"row-striping row-hover\">\n<tr class=\"row-2\">\n\t<td class=\"column-1\">Every HTML route returns the header<\/td><td class=\"column-2\">DevTools \u2192 Network \u2192 Doc, on each route<\/td>\n<\/tr>\n<tr class=\"row-3\">\n\t<td class=\"column-1\">The directive is built for the current shop<\/td><td class=\"column-2\">Confirm the installed shop domain appears<\/td>\n<\/tr>\n<tr class=\"row-4\">\n\t<td class=\"column-1\">No wildcard (STATIC_FRAME_ANCESTORS); no missing header (MISSING_EMBEDDED_CSP)<\/td><td class=\"column-2\">Inspect response headers<\/td>\n<\/tr>\n<tr class=\"row-5\">\n\t<td class=\"column-1\">Embedding config matches behavior<\/td><td class=\"column-2\">Embedded apps stay embedded; standalone apps aren\u2019t configured as embedded<\/td>\n<\/tr>\n<tr class=\"row-6\">\n\t<td class=\"column-1\">Redirects use App Bridge<\/td><td class=\"column-2\">Follow a redirect inside the admin<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<!-- #tablepress-506 from cache -->\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Second Store Test:<\/strong> Load the app from two stores and compare headers. The shop domain should change with the store.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"How_Should_You_Handle_Shopify_Compliance_Webhooks\"><\/span>How Should You Handle Shopify &amp; Compliance Webhooks?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Subscribe to the three mandatory compliance webhooks (<code>customers\/data_request<\/code>, <code>customers\/redact <\/code>and <code>shop\/redact<\/code>), verify the HMAC on every request, and return 401 when it\u2019s invalid.<\/p>\n\n\n\n<div id=\"tablepress-507-scroll-wrapper\" class=\"tablepress-scroll-wrapper\">\n<table id=\"tablepress-507\" class=\"tablepress tablepress-id-507 column1-color tablepress-responsive\">\n<thead>\n<tr class=\"row-1\">\n\t<th class=\"column-1\">Check<\/th><th class=\"column-2\">How to test it<\/th>\n<\/tr>\n<\/thead>\n<tbody class=\"row-striping row-hover\">\n<tr class=\"row-2\">\n\t<td class=\"column-1\">All three compliance topics are subscribed<\/td><td class=\"column-2\">Review webhook config<\/td>\n<\/tr>\n<tr class=\"row-3\">\n\t<td class=\"column-1\">Endpoints use valid HTTPS<\/td><td class=\"column-2\">Confirm no plain-HTTP callbacks<\/td>\n<\/tr>\n<tr class=\"row-4\">\n\t<td class=\"column-1\">HMAC is verified against the raw body before JSON parsing<\/td><td class=\"column-2\">Send an invalid HMAC; expect 401<\/td>\n<\/tr>\n<tr class=\"row-5\">\n\t<td class=\"column-1\">Handlers are idempotent<\/td><td class=\"column-2\">Replay the same delivery ID; confirm no duplicate processing<\/td>\n<\/tr>\n<tr class=\"row-6\">\n\t<td class=\"column-1\">data_request returns the data; the redact topics delete or anonymize it<\/td><td class=\"column-2\">Trigger each with Shopify CLI; inspect the database, backups, exports, queues and downstream services<\/td>\n<\/tr>\n<tr class=\"row-7\">\n\t<td class=\"column-1\">Requests finish within 30 days unless law requires retention<\/td><td class=\"column-2\">Time a full redaction<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<!-- #tablepress-507 from cache -->\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Second store test<\/strong><em>:<\/em> trigger <code>customers\/redact<\/code> for Store A and confirm Store B\u2019s data is untouched.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"What_Data_Protections_will_Shopify_Expect_from_Your_App\"><\/span>What Data Protections will Shopify Expect from Your App?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Least-privilege scopes, a protected customer data request if you use customer data, and personal data that is minimized, encrypted and deleted on schedule. For the level rules, see our <a href=\"https:\/\/www.getastra.com\/blog\/penetration-testing\/shopify-app-store-requirements\/\">Shopify App Store requirements<\/a> guide.<\/p>\n\n\n\n<div id=\"tablepress-508-scroll-wrapper\" class=\"tablepress-scroll-wrapper\">\n<table id=\"tablepress-508\" class=\"tablepress tablepress-id-508 column1-color tablepress-responsive\">\n<thead>\n<tr class=\"row-1\">\n\t<th class=\"column-1\">Check<\/th><th class=\"column-2\">How to test it<\/th>\n<\/tr>\n<\/thead>\n<tbody class=\"row-striping row-hover\">\n<tr class=\"row-2\">\n\t<td class=\"column-1\">Every scope maps to a shipped feature and a real API call (SCOPE_OVER_REQUEST)<\/td><td class=\"column-2\">Build a matrix: scope \u2192 API call \u2192 feature \u2192 justification<\/td>\n<\/tr>\n<tr class=\"row-3\">\n\t<td class=\"column-1\">Unused and template-inherited scopes are removed; write scopes only where read won\u2019t do<\/td><td class=\"column-2\">Search the codebase for each scope\u2019s calls<\/td>\n<\/tr>\n<tr class=\"row-4\">\n\t<td class=\"column-1\">Sensitive scopes like read_all_orders are justified; optional scopes are used where possible<\/td><td class=\"column-2\">Deny optional scopes and run the app<\/td>\n<\/tr>\n<tr class=\"row-5\">\n\t<td class=\"column-1\">Protected customer data access is requested in the Partner Dashboard<\/td><td class=\"column-2\">Confirm the request matches the fields you use<\/td>\n<\/tr>\n<tr class=\"row-6\">\n\t<td class=\"column-1\">Personal data is encrypted in transit and at rest, access-limited, logged and retained for a defined period<\/td><td class=\"column-2\">Inspect the database, access controls and retention jobs<\/td>\n<\/tr>\n<tr class=\"row-7\">\n\t<td class=\"column-1\">No production customer data in dev or staging<\/td><td class=\"column-2\">Check staging data sources<\/td>\n<\/tr>\n<tr class=\"row-8\">\n\t<td class=\"column-1\">Deletion works<\/td><td class=\"column-2\">Seed a development store, trigger redaction, verify<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<!-- #tablepress-508 from cache -->\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Second Store Test:<\/strong> Use Store A\u2019s access token against Store B\u2019s resources. It should fail, and your scope list should show what a stolen token could do.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"How_Does_the_OWASP_Top_10_Apply_to_Shopify_App_Security\"><\/span>How Does the OWASP Top 10 Apply to Shopify App Security?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Each OWASP risk has a Shopify form, and most of them end up as cross-store problems. Shopify\u2019s check IDs are in the third column.<\/p>\n\n\n\n<div id=\"tablepress-509-scroll-wrapper\" class=\"tablepress-scroll-wrapper\">\n<table id=\"tablepress-509\" class=\"tablepress tablepress-id-509 column1-color tablepress-responsive\">\n<thead>\n<tr class=\"row-1\">\n\t<th class=\"column-1\">OWASP risk<\/th><th class=\"column-2\">Shopify version<\/th><th class=\"column-3\">Shopify check IDs<\/th><th class=\"column-4\">How to test it<\/th>\n<\/tr>\n<\/thead>\n<tbody class=\"row-striping row-hover\">\n<tr class=\"row-2\">\n\t<td class=\"column-1\">Broken access control<\/td><td class=\"column-2\">Cross-shop IDOR: altered shop, customer, order or GraphQL IDs<\/td><td class=\"column-3\">MISSING_AUTHORIZATION_CHECK, MISSING_TENANT_ISOLATION<\/td><td class=\"column-4\">Request Store B\u2019s resources as Store A<\/td>\n<\/tr>\n<tr class=\"row-3\">\n\t<td class=\"column-1\">Cryptographic failures<\/td><td class=\"column-2\">Secrets in Git, bundles, source maps or logs<\/td><td class=\"column-3\">COMMITTED_SECRET, CREDENTIAL_LOG_LEAKAGE, CREDENTIAL_BROWSER_LEAKAGE<\/td><td class=\"column-4\">Scan Git history, built JS and logs<\/td>\n<\/tr>\n<tr class=\"row-4\">\n\t<td class=\"column-1\">Injection<\/td><td class=\"column-2\">SQL injection, XSS, unsafe HTML, Liquid injection, XSS in theme app extensions<\/td><td class=\"column-3\">SQL_INJECTION, CROSS_SITE_SCRIPTING, UNSAFE_INNERHTML, APP_PROXY_LIQUID_INJECTION, THEME_EXTENSION_XSS<\/td><td class=\"column-4\">Fuzz titles, metafields, settings, search and app-proxy inputs; store a script payload in a setting and load the storefront<\/td>\n<\/tr>\n<tr class=\"row-5\">\n\t<td class=\"column-1\">Insecure design<\/td><td class=\"column-2\">Unsigned app-proxy requests<\/td><td class=\"column-3\">APP_PROXY_UNVERIFIED_SIGNATURE<\/td><td class=\"column-4\">Send an unsigned proxy request<\/td>\n<\/tr>\n<tr class=\"row-6\">\n\t<td class=\"column-1\">Security misconfiguration<\/td><td class=\"column-2\">Wildcard or missing CSP, open admin panels, permissive CORS<\/td><td class=\"column-3\">STATIC_FRAME_ANCESTORS, MISSING_EMBEDDED_CSP<\/td><td class=\"column-4\">Inspect headers, CORS and staging access<\/td>\n<\/tr>\n<tr class=\"row-7\">\n\t<td class=\"column-1\">Vulnerable components<\/td><td class=\"column-2\">A reachable vulnerable dependency<\/td><td class=\"column-3\">DEPENDENCY_REACHABILITY<\/td><td class=\"column-4\">Run SCA; confirm the function is reachable<\/td>\n<\/tr>\n<tr class=\"row-8\">\n\t<td class=\"column-1\">Authentication failures<\/td><td class=\"column-2\">Weak OAuth, replayable sessions or webhooks<\/td><td class=\"column-3\">SESSION_LIFECYCLE_AND_REPLAY, UNAUTHENTICATED_ENDPOINT<\/td><td class=\"column-4\">Replay tokens and webhooks<\/td>\n<\/tr>\n<tr class=\"row-9\">\n\t<td class=\"column-1\">Integrity failures<\/td><td class=\"column-2\">Request-controlled ScriptTag URL, unsafe uploads<\/td><td class=\"column-3\">SCRIPT_TAG_URL_INJECTION<\/td><td class=\"column-4\">Try injecting a script URL or malicious upload<\/td>\n<\/tr>\n<tr class=\"row-10\">\n\t<td class=\"column-1\">Logging failures<\/td><td class=\"column-2\">Credentials in logs, no access trail<\/td><td class=\"column-3\">CREDENTIAL_LOG_LEAKAGE<\/td><td class=\"column-4\">Send malformed requests; inspect logs<\/td>\n<\/tr>\n<tr class=\"row-11\">\n\t<td class=\"column-1\">SSRF<\/td><td class=\"column-2\">App proxy, importer, URL preview or image fetch that requests attacker-controlled URLs<\/td><td class=\"column-3\">SSRF_REQUEST_FORGERY<\/td><td class=\"column-4\">Test localhost, private IPs, cloud metadata and redirect chains<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<!-- #tablepress-509 from cache -->\n\n\n\n<p class=\"wp-block-paragraph\">3 Shopify-specific scenarios require their own tests specifically:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Cross-shop IDOR:<\/strong> A query takes <code>shop<\/code> from request input instead of the session. Change it and see whose data comes back.<\/li>\n\n\n\n<li><strong>App-proxy SSRF:<\/strong> An endpoint fetches an attacker-supplied URL. Point it at an internal address.<\/li>\n\n\n\n<li><strong>XSS in theme app extensions:<\/strong> User-controlled text or metafield output renders as unsafe HTML on the storefront (<code>THEME_EXTENSION_XSS<\/code>). Store a script payload in a setting and load the storefront.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Second Store Test:<\/strong> Take a resource ID from Store B and request it as Store A through every surface: REST, GraphQL, app proxy and exports.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"What_Operational_Security_Should_Be_in_Place_Before_You_Submit\"><\/span>What Operational Security Should Be in Place Before You Submit?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Keep secrets out of code, keep environments separate, and have a plan for the day a token leaks. Shopify scans your app for open ports with Nmap during review, so unnecessary public services are a review issue as well as a security one.<\/p>\n\n\n\n<div id=\"tablepress-510-scroll-wrapper\" class=\"tablepress-scroll-wrapper\">\n<table id=\"tablepress-510\" class=\"tablepress tablepress-id-510 column1-color tablepress-responsive\">\n<thead>\n<tr class=\"row-1\">\n\t<th class=\"column-1\">Check<\/th><th class=\"column-2\">How to test it<\/th>\n<\/tr>\n<\/thead>\n<tbody class=\"row-striping row-hover\">\n<tr class=\"row-2\">\n\t<td class=\"column-1\">Secrets live in a secrets manager and rotate without a full redeploy<\/td><td class=\"column-2\">Search Git history, built JS and source maps; time a rotation<\/td>\n<\/tr>\n<tr class=\"row-3\">\n\t<td class=\"column-1\">Production, staging and development are isolated, with no shared OAuth clients or keys<\/td><td class=\"column-2\">Confirm staging can\u2019t reach production data<\/td>\n<\/tr>\n<tr class=\"row-4\">\n\t<td class=\"column-1\">Databases, queues, admin panels and internal services aren\u2019t public<\/td><td class=\"column-2\">Run an external port scan<\/td>\n<\/tr>\n<tr class=\"row-5\">\n\t<td class=\"column-1\">MFA is enforced on source control, cloud consoles, package registries and admin tools<\/td><td class=\"column-2\">Check each account\u2019s settings<\/td>\n<\/tr>\n<tr class=\"row-6\">\n\t<td class=\"column-1\">Dependencies are minimal, pinned and monitored<\/td><td class=\"column-2\">Review alerts; confirm whether flagged code is reachable<\/td>\n<\/tr>\n<tr class=\"row-7\">\n\t<td class=\"column-1\">A public security contact and disclosure page exist<\/td><td class=\"column-2\">Confirm the page is live<\/td>\n<\/tr>\n<tr class=\"row-8\">\n\t<td class=\"column-1\">An incident-response plan covers containment, credential revocation, evidence, Shopify notification and customer impact<\/td><td class=\"column-2\">Run a leaked-token tabletop<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<!-- #tablepress-510 from cache -->\n\n\n\n<p class=\"wp-block-paragraph\">Shopify\u2019s CTP program requires many of these for Plus apps. See <a href=\"https:\/\/www.getastra.com\/blog\/penetration-testing\/shopify-app-store-requirements\/#What_are_the_Shopify_App_Store_security_requirements_at_each_tier\">how the tiers compare<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Second Store Test:<\/strong> In your leaked-token tabletop, count how many stores one token exposes and how long revocation takes.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"What_Does_Shopifys_AI_Self-Review_Catch_and_What_Cant_It\"><\/span>What Does Shopify\u2019s AI Self-Review Catch, and What Can\u2019t It?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Simply put, it\u2019s a good first filter and a bad finish line. The self-review reads your local code, so it can judge what\u2019s visible there and nothing else. Shopify\u2019s pre-review tooling excludes anything that depends on listing content, live app behaviour, or merchant-facing UX, much of what makes an app work.&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Shopify\u2019s developer team also recommends its Dev MCP for catching common issues before the review team flags them, and it\u2019s worth using.&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Shopify\u2019s \u2018<code>shopify app security check<\/code>\u2019 combines two things:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Deterministic scans:<\/strong> fixed checks with exact answers, like whether an API version is end-of-life.\u00a0<\/li>\n\n\n\n<li><strong>Agent review:<\/strong> deeper checks, such as whether a database query is scoped to one shop.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">What neither can do, in our view, is attack your running app. They won\u2019t run multi-store authorization attacks, abuse your business logic, test SSRF against live infrastructure, or confirm that deletion actually happened in every system holding the data.&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here\u2019s how we\u2019d layer it:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Automated checks:<\/strong> Shopify\u2019s tooling, dependency scanning, secret scanning and CI security tests.<\/li>\n\n\n\n<li><strong>AI self-review:<\/strong> a fast gate for requirements judged from code.<\/li>\n\n\n\n<li><strong>Manual pentest:<\/strong> a human attacking tenant isolation, OAuth edge cases, business logic, SSRF and webhooks.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">A pentest isn\u2019t a necessity for most apps per guidelines, with CTP as the only primary exception, but no tool reading your code can play Store B against your running app, as a human tester can.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Second Store Test:<\/strong> Run the first two layers, then pick the three highest-risk cross-store checks above and test them against the live app. If one store can reach another, you\u2019ve found your first fix. If none can, you have evidence worth keeping.&nbsp;<\/p>\n\n\n\n<h2 id=\"review-timelines\" class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"How_Long_Does_Shopify_App_Review_Take_and_How_Do_You_Avoid_Resubmission_Loops\"><\/span>How Long Does Shopify App Review Take, and How Do You Avoid Resubmission Loops?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Shopify\u2019s documentation doesn\u2019t give a fixed review time &amp; in our opinion, be wary of any advisory or article that does. In February 2026, its developer team reported reviews were taking longer than usual, with waits extending up to a month or more (even when submission emails continued to religiously promise up to 14 business days).\u00a0<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Check Shopify\u2019s current announcements before planning a launch (as of October 2026).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Your app moves through Draft, Submitted, Reviewed and Published, and Shopify assigns a reviewer once core requirements are met. You can withdraw at any point, but most of the control you have actually exists before you press submit.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Three rules which help you avoid resubmission loops:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Don\u2019t resubmit while you wait.<\/strong> It is very tempting, but per Shopify experts, doing so moves you to the back of the queue.<a href=\"https:\/\/community.shopify.dev\/t\/longer-app-store-review-times-what-you-need-to-know\/31728\" target=\"_blank\" rel=\"noopener\">\u00a0<\/a><\/li>\n\n\n\n<li><strong>Don\u2019t fail repeatedly.<\/strong> Apps that repeatedly fail review may face a temporary suspension, so a rushed second attempt often costs more delay than it was meant to save.<\/li>\n\n\n\n<li><strong>Fix the avoidable failures first.<\/strong> As dull as they may be, per Shopify\u2019s <code>common-problems list<\/code>, these are the highest risk:\n<ul class=\"wp-block-list\">\n<li>No OAuth redirect after install, or a fatal error after install<\/li>\n\n\n\n<li>An app that doesn\u2019t reinstall<\/li>\n\n\n\n<li>Embedded and non-embedded switching<\/li>\n\n\n\n<li>Broken UI, or 404, 500 and 300-series errors<\/li>\n\n\n\n<li>Missing testing instructions or credentials<\/li>\n\n\n\n<li>Billing that doesn\u2019t use Shopify\u2019s required system<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Therefore, run this gate before every submission:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Run Shopify\u2019s automated checks and AI self-review.<\/li>\n\n\n\n<li>Run Shopify app security check.<\/li>\n\n\n\n<li>Test install, OAuth, reinstall, billing, UI, webhooks and uninstall on a development store.<\/li>\n\n\n\n<li>Prepare a reviewer pack: test credentials, a demo store, a short screencast, a feature walkthrough and scope justifications.<\/li>\n\n\n\n<li>Fix every requirement-level failure before resubmitting.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Second Store Test:<\/strong> Run the full gate on two development stores before you submit.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"How_can_Astra_help_with_Shopify_App_Security\"><\/span>How can Astra help with Shopify App Security?&nbsp;<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">You can run most of this checklist yourself; the part that\u2019s hard to do alone is having someone else attack your app.\u00a0<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1507\" height=\"1600\" src=\"\/cdn-cgi\/image\/quality=80,format=auto,onerror=redirect,metadata=none\/https:\/\/cdn-blog.getastra.com\/2026\/01\/69030f77-image.png\" alt=\"Astra Security's automated DAST tool + VAPT platform dashboard\" class=\"wp-image-45051\" srcset=\"\/cdn-cgi\/image\/quality=80,format=auto,onerror=redirect,metadata=none\/https:\/\/cdn-blog.getastra.com\/2026\/01\/69030f77-image.png 1507w, \/cdn-cgi\/image\/width=1447,height=1536,fit=crop,quality=80,format=auto,onerror=redirect,metadata=none\/https:\/\/cdn-blog.getastra.com\/2026\/01\/69030f77-image.png 1447w\" sizes=\"auto, (max-width: 1507px) 100vw, 1507px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">That\u2019s where Astra comes in. Think of it as an independent security test before submission:&nbsp;<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Share your testing scope, including OAuth flows, webhooks, app proxies, and REST and GraphQL APIs.\u00a0<\/li>\n\n\n\n<li>Make sure cross-store access is explicitly included in the testing requirements.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Astra\u2019s certified penetration testers assess your live app for security vulnerabilities, including business logic flaws that automated scans may miss.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You receive a detailed report with identified vulnerabilities and remediation guidance. Once you&#8217;ve addressed the findings, free rescans help verify the fixes alongside a publicly verifiable security certificate on our proprietary Trust Center.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Want an independent test before Shopify\u2019s review team does it? <a href=\"https:\/\/www.getastra.com\/pentesting\/shopify-app\"><strong>Talk to a Security Expert<\/strong> \u2192&nbsp;<\/a><\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Final_Thoughts\"><\/span>Final Thoughts<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Good Shopify app security means you can say two things before you submit: one store can\u2019t reach another\u2019s data, and you have the evidence. Run each check from a second development store, keep what you find, and submit once.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"FAQs\"><\/span>FAQs<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n<div id=\"rank-math-faq\" class=\"rank-math-block\">\n<div class=\"rank-math-list \">\n<div id=\"faq-question-1793908823890\" class=\"rank-math-list-item\">\n<h3 class=\"rank-math-question \"><strong>How long does Shopify app review take?<\/strong><\/h3>\n<div class=\"rank-math-answer \">\n\n<p>Shopify\u2019s documentation doesn\u2019t give a fixed time. In February 2026, its developer team said reviews were taking longer than usual, and developers in that thread reported waits of a month or more. Check Shopify\u2019s community announcements for the current picture before you plan a launch.<\/p>\n\n<\/div>\n<\/div>\n<div id=\"faq-question-1793908844526\" class=\"rank-math-list-item\">\n<h3 class=\"rank-math-question \"><strong>What happens if my app fails Shopify review?<\/strong><\/h3>\n<div class=\"rank-math-answer \">\n\n<p>You fix the issues and resubmit. Shopify has said resubmitting while you wait moves you to the back of the queue, and developers in the same thread say fixes after a failed review do too. Repeated failures can also lead to a temporary suspension.<\/p>\n\n<\/div>\n<\/div>\n<div id=\"faq-question-1793908856985\" class=\"rank-math-list-item\">\n<h3 class=\"rank-math-question \"><strong>Does Shopify require a pentest before app review?<\/strong><\/h3>\n<div class=\"rank-math-answer \">\n\n<p>No. Only the Certified Technology Partner program names an annual independent pentest. Shopify\u2019s own checks read your code rather than attack your running app, so an independent test, in our view, covers what they can\u2019t. Our Shopify App Store requirements guide explains which program applies.<\/p>\n\n<\/div>\n<\/div>\n<div id=\"faq-question-1793908871549\" class=\"rank-math-list-item\">\n<h3 class=\"rank-math-question \"><strong>Can you test Shopify app security before submitting?<\/strong><\/h3>\n<div class=\"rank-math-answer \">\n\n<p>Yes. Shopify recommends testing on a development store before you submit, and its developer team points to Dev MCP for catching common issues early. Repeat each check from a second development store to see whether one store can reach another\u2019s data, and keep the results.<\/p>\n\n<\/div>\n<\/div>\n<div id=\"faq-question-1793908890374\" class=\"rank-math-list-item\">\n<h3 class=\"rank-math-question \"><strong>What are Shopify\u2019s mandatory compliance webhooks?<\/strong><\/h3>\n<div class=\"rank-math-answer \">\n\n<p>Apps must subscribe to customers\/data_request, customers\/redact and shop\/redact. Handlers verify the HMAC signature and return 401 when it\u2019s invalid. Test each one end to end, so you know data is actually returned or deleted, not just acknowledged with a 200.<\/p>\n\n<\/div>\n<\/div>\n<div id=\"faq-question-1793908904034\" class=\"rank-math-list-item\">\n<h3 class=\"rank-math-question \"><strong>Want an independent test before you submit?<\/strong><\/h3>\n<div class=\"rank-math-answer \">\n\n<p>A checklist covers what you know to test. A pentest goes after what you didn\u2019t think of, including crossing from one store to another. Astra\u2019s manual pentests (PTaaS) are run by certified pentesters who look for business logic flaws. You get a report, free rescans, and a publicly verifiable certificate after remediation.<\/p>\n\n<\/div>\n<\/div>\n<\/div>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>Key Takeaways Most teams treat Shopify app review as a formality until it isn\u2019t. In February 2026, Shopify\u2019s developer team told the community that review times were longer than usual, and that resubmitting sends you to the back of the queue. Moreover, it\u2019s easy enough to catch a failed install on your own. What\u2019s easier &#8230; <a title=\"Shopify App Security Checklist: What to Test Before App Store Review\" class=\"read-more\" href=\"https:\/\/www.getastra.com\/blog\/security-audit\/shopify-app-security-checklist\/\" aria-label=\"Read more about Shopify App Security Checklist: What to Test Before App Store Review\">Read more<\/a><\/p>\n","protected":false},"author":111,"featured_media":50048,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[340],"tags":[],"class_list":["post-50035","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-security-audit"],"_links":{"self":[{"href":"https:\/\/www.getastra.com\/blog\/wp-json\/wp\/v2\/posts\/50035","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.getastra.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.getastra.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.getastra.com\/blog\/wp-json\/wp\/v2\/users\/111"}],"replies":[{"embeddable":true,"href":"https:\/\/www.getastra.com\/blog\/wp-json\/wp\/v2\/comments?post=50035"}],"version-history":[{"count":2,"href":"https:\/\/www.getastra.com\/blog\/wp-json\/wp\/v2\/posts\/50035\/revisions"}],"predecessor-version":[{"id":50258,"href":"https:\/\/www.getastra.com\/blog\/wp-json\/wp\/v2\/posts\/50035\/revisions\/50258"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.getastra.com\/blog\/wp-json\/wp\/v2\/media\/50048"}],"wp:attachment":[{"href":"https:\/\/www.getastra.com\/blog\/wp-json\/wp\/v2\/media?parent=50035"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.getastra.com\/blog\/wp-json\/wp\/v2\/categories?post=50035"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.getastra.com\/blog\/wp-json\/wp\/v2\/tags?post=50035"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}