Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/remove-shopify-api-key-secret-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/app': patch
---

Remove app doctor detection of generic 32-character Shopify API keys.
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,6 @@ interface SecretPattern {
}

export const SECRET_PATTERNS: SecretPattern[] = [
// Shopify API key (32 hex chars)
{
regex: /(?:api[_-]?key|client[_-]?id|SHOPIFY_API_KEY)\s*[:=]\s*['"]([a-f0-9]{32})['"]/i,
name: 'Shopify API key',
},
// Shopify API secret (shpss_ prefix or 32 hex)
{
regex: /(?:api[_-]?secret|SHOPIFY_API_SECRET)\s*[:=]\s*['"](shpss_[a-f0-9]+|[a-f0-9]{32})['"]/i,
Expand Down Expand Up @@ -132,13 +127,6 @@ export function redactText(text: string): string {

const ENV_FILE_PATTERN = /(^|\/)\.env(?:\.[^/]+)?$/
const NAMED_SECRET_FILE_PATTERN = /(^|\/)(?:\.env\.(?:secrets|keys)|(?:secrets|credentials)\.json)$/
const SHOPIFY_APP_TOML = /(^|\/)shopify\.app(?:\.[^/]+)?\.toml$/

/** `client_id` in shopify.app.toml is the public app client ID the CLI writes. */
function isPublicAppTomlClientId(filePath: string, pattern: SecretPattern): boolean {
return pattern.name === 'Shopify API key' && SHOPIFY_APP_TOML.test(filePath)
}

const SECRET_ASSIGNMENT_PATTERN =
/(?:api[_-]?key|api[_-]?secret|access[_-]?token|secret[_-]?key|private[_-]?key|password|SHOPIFY_API_KEY|SHOPIFY_API_SECRET)\s*[:=]\s*(?:"[^"\r\n]+"|'[^'\r\n]+'|[^\s#'"\r\n][^#\r\n]*)/i

Expand Down Expand Up @@ -201,7 +189,6 @@ export async function scanCommittedSecrets(secretEvidenceFiles: SourceFile[], ap
for (const pattern of SECRET_PATTERNS) {
// Patterns are non-global so `.test()` has no lastIndex state to leak
// between iterations. Do not add the /g flag here.
if (isPublicAppTomlClientId(file.path, pattern)) continue
if (!pattern.regex.test(line)) continue
issues.push({
id: 'COMMITTED_SECRET',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ describe('redaction never emits the secret it detected', () => {
['GitHub token', `const k = "${PROBES.githubToken}";`],
['Google API key', `const k = "${PROBES.googleKey}";`],
['Slack token', `const k = "${PROBES.slackToken}";`],
['Shopify API key', `const apiKey = "${HEX32}";`],
]

for (const [label, line] of samples) {
Expand Down Expand Up @@ -268,7 +267,7 @@ describe('git status drives severity, not .gitignore text', () => {
})

describe('secret evidence coverage', () => {
test('does not flag the public client_id in shopify.app.toml', async () => {
test('does not flag generic 32-character hex client IDs as Shopify API keys', async () => {
const dir = makeApp({
'app.js': `const client_id = "${HEX32}";\n`,
})
Expand All @@ -279,11 +278,11 @@ describe('secret evidence coverage', () => {
writeFileSync(join(dir, 'shopify.app.staging.toml'), `name = "s"\nclient_id = "${HEX32}"\n`)
const result = await scan(dir)
const secrets = result.issues.filter((issue) => issue.id === 'COMMITTED_SECRET')
expect(
secrets.some((issue) => issue.location.file === 'shopify.app.toml' && issue.title.includes('Shopify API key')),
).toBe(false)
expect(secrets.some((issue) => issue.location.file === 'shopify.app.toml' && issue.title.includes('API key'))).toBe(
false,
)
expect(secrets.some((issue) => issue.location.file === 'shopify.app.staging.toml')).toBe(false)
expect(secrets.some((issue) => issue.location.file === 'app.js')).toBe(true)
expect(secrets.some((issue) => issue.location.file === 'app.js')).toBe(false)
Comment on lines 280 to +285
expect(
secrets.some((issue) => issue.location.file === 'shopify.app.toml' && issue.title.includes('Shopify API secret')),
).toBe(true)
Expand Down Expand Up @@ -344,8 +343,6 @@ describe('incomplete coverage is reported, not hidden', () => {
/** Probe strings with realistic shape, assembled at runtime. See note above. */
function probeFor(name: string): string | undefined {
switch (name) {
case 'Shopify API key':
return `api_key = "${HEX32}"`
case 'Shopify API secret':
return `api_secret = "${PROBES.shopifySecret}"`
case 'Shopify access token':
Expand Down
Loading