A black-box test answers one question: what can someone do to your app from the outside? It is the right starting point, and it is how an attacker sees the app. But it has a blind spot. A tester can only reach the endpoints they discover, with the data they can create, in the time available. A missing store filter in a rarely used export, an access token committed to git three years ago or a webhook handler that is never called in staging can all stay invisible.

When a team gives us read access to the repository, the review becomes grey-box: we test the running app and also read how it is built. This article explains what that adds for a Shopify app, which tools we use, what we check by hand and how to share code safely.

The short version

  • Source access finds issues that black-box testing rarely reaches: secrets in git history, vulnerable dependencies, and authorization gaps in code paths that are hard to trigger from outside.
  • Scanners do the broad sweep. The Shopify-specific part, such as tenant isolation, HMAC verification and OAuth handling, needs custom rules and manual review.
  • Every automated result is triaged by a person. Duplicates are merged and false positives are listed separately, so the report contains confirmed issues with file and line references.
  • We need read-only, time-limited access. We do not change your code; your team implements fixes.

Why source access changes the result

In a Shopify app, many serious bugs come down to a check that is missing. They are not unusual inputs that a fuzzer can trip over. A handler that loads a record without the store, a webhook route that skips verification or a callback that accepts any shop value all return normal-looking responses. From outside, finding them depends on guessing the right endpoint and the right identifier. In the code, they are visible as a pattern, and the same pattern can be searched for across the whole codebase.

Source access also covers things that have no external surface at all: credentials in the repository, outdated libraries, debug tooling enabled by configuration, and data written to logs. None of these show up in an HTTP proxy, but each can turn a small incident into a large one.

Step 1: Secrets in the repository and its history

We scan the entire git history, not only the current branch. A key deleted in a later commit is still in every clone of the repository, so removing it from the code does not fix the leak. Only rotating the key does.

What we look for:

  • Shopify API secrets and access tokens, payment and email provider keys, cloud credentials, database URLs.
  • Files that should never be tracked: .env variants, private keys and certificates (.pem, .p12), service account JSON, token.json, SSH keys.
  • Secrets in the working copy that were never committed but would be included in a build or a support archive.

Tools: Gitleaks across all commits, with matches redacted in our notes, plus Trivy and TruffleHog on the working tree. Every match is checked by hand: test fixtures and example values are common, and a real live credential is always reported as critical, with rotation as the first step.

Step 2: Dependencies with known vulnerabilities

A typical Shopify app has a PHP or Node backend, a React frontend and hundreds of transitive packages. We read the lockfiles (composer.lock, package-lock.json, yarn.lock) with OSV-Scanner, Trivy, npm audit and composer audit, and check framework and runtime versions against their support schedules.

Raw dependency output is long and noisy, so this step is mostly triage:

  • Is the vulnerable package used in production, or only in development tooling?
  • Is the vulnerable function reachable from the app’s code paths?
  • Is a fixed version available, and does upgrading require a major version change?

The report lists the issues that matter for your app, not the full scanner output. An end-of-life framework or PHP version is reported on its own, because it means future fixes will not arrive at all.

Step 3: Static analysis with Shopify-specific rules

General rule sets catch general bugs: SQL injection, command injection, unsafe deserialization, XSS sinks. We run Semgrep with the standard security rule sets for PHP, Laravel and JavaScript, and add our own rules for patterns that matter specifically in Shopify apps:

A rule match is a lead, not a finding. Static analysis cannot always see that a store filter is applied by a base class, a middleware or a global scope, so each match is checked in context. When a project uses a global scope correctly, we record that and drop the related matches. Hiding them in the report would make it harder to read.

Here is the kind of webhook issue these rules are designed to catch:

// Vulnerable: the HMAC is computed over re-encoded JSON, not the bytes Shopify signed
$expected = base64_encode(hash_hmac('sha256', json_encode($request->all()), $secret, true));
if ($expected != $request->header('X-Shopify-Hmac-Sha256')) abort(401);

Re-encoding changes the bytes, so verification fails on legitimate events, and teams often “fix” that by disabling the check. The loose != comparison is a second problem. The correct version uses the raw body and a constant-time comparison:

// Verified: raw request body and hash_equals
$expected = base64_encode(hash_hmac('sha256', $request->getContent(), $secret, true));
if (! hash_equals($expected, (string) $request->header('X-Shopify-Hmac-Sha256'))) abort(401);

Alongside the scanners, our in-house AI-assisted tooling helps map routes, handlers and data access across the codebase, so we can compare how store context is checked from one endpoint to the next. It speeds up the reading. It does not replace it: every reported issue is confirmed by a person.

Step 4: What we check by hand

Some of the most important questions cannot be answered by a pattern match. We review these manually in every source code engagement:

Every webhook endpoint. Is the HMAC verified on the raw body, with a constant-time comparison, before any processing? Is the shop taken from the verified request rather than from the payload? Are compliance webhooks (customers/data_request, customers/redact, shop/redact) covered by the same verification?

The OAuth callback. Is the shop value validated as a myshopify.com domain before it is used? Are the callback HMAC and the state nonce checked? Can the redirect target be influenced by request input?

Tenant isolation model. Is store scoping enforced in one place, such as a global scope, a repository layer or row-level security, or added by hand to each query? If it is manual, we look for the places where it was forgotten. Background jobs, exports and admin tooling are the usual suspects. See our article on cross-store IDOR in Shopify apps for the patterns.

Debug and admin tooling in production. For Laravel apps: are Telescope and Horizon reachable in production, and are their access gates restrictive? Is APP_DEBUG off and APP_ENV set to production? The same applies to any internal dashboard or queue UI.

Requested scopes versus used scopes. We compare the access scopes in shopify.app.toml with the API calls the code actually makes. Unused write scopes increase the damage a leaked token can do, and reviewers ask about them.

Compliance webhooks that do nothing. A shop/redact handler that returns 200 without deleting data passes a basic check, but it does not meet the commitment the app makes to merchants.

Versions and support status. Framework, language runtime and key libraries, compared with their end-of-life dates.

How findings are reported

Scanners overlap: the same hardcoded key may be reported by three tools, and the same missing check may match two rules. We deduplicate and group findings by class of problem, not by tool, so the report reads as a list of things to fix:

  • Critical first: live credentials in the repository or its history, webhooks without verification, confirmed gaps in tenant isolation.
  • High and medium grouped by area: authentication, authorization, integrations, dependencies, configuration.
  • For each finding: file and line, why it matters for a Shopify app, and a specific fix.
  • A separate section for false positives we dropped and why, so your team can see what was checked and does not have to re-investigate it.

Where possible, code-level findings are linked to the dynamic test. A missing store filter found in code is confirmed with a cross-store request between two test stores, which shows its real impact.

How to share code safely

You do not need to give us broad access to your organization:

  • A read-only deploy key or a temporary collaborator role on the specific repositories in scope, removed when the engagement ends. A one-off archive or bundle of the repository, including history, also works.
  • No production secrets. If .env files are tracked, that is a finding. We do not need you to send them separately.
  • Code is analysed locally and is not uploaded to external scanning services without your permission. Our copy is deleted as soon as the analysis is complete.

Source code review is part of an engagement only when access is provided and it is included in the agreed scope.

When it is worth adding source access

Source access is most useful when:

  • The app has many endpoints, background jobs or integrations, so black-box coverage is necessarily a sample.
  • Shopify has asked for an incident report, and you need confidence that the root cause is not repeated elsewhere in the code.
  • The codebase has changed hands, or has been in development for years with several contributors.
  • You want fixes verified at the code level as well as by retesting behaviour.

Our Shopify App Security Readiness review and Shopify App Incident VAPT can both include source code review when you provide access. If you have already fixed reported issues, the Retest & Evidence Pack can confirm the changes in both code and behaviour.