Back to skill

Security audit

WeChat to Shopify Blog

Security checks for vulnerabilities and agentic risk

Overview

The skill is mostly coherent for making Shopify blog drafts from WeChat articles, but it requests broad Shopify access and reads more store data than the core task clearly needs.

Install only if you are comfortable giving this skill Shopify product-read access plus content/file write access. Use it on an owned store, keep skill-hub.env private, prefer the browser-based Shopify CLI flow, review the draft preview before any --execute command, and consider limiting or revising the product-context and write-scope behavior before routine use.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (5)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/fetch-wechat-article.mjs:211
Finding

Redirect-Based SSRF in WeChat Article and Image Retrieval

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/shopify-context.mjs:65
Finding

Redirect-Based SSRF During Shopify Homepage Metadata Retrieval

Content
View full analysis
]*>([\s\S]*?)<\/title>/i)?.[1]?.replace(/\s+/g, " ").trim() || ""; const description = html.match(/]+name=["']description["'][^>]+content=["']([^"']*)["'][^>]*>/i)?.[1]?.trim() || html.match(/]+content=["']([^"']*)["'][^>]+name=["']description["'][^>]*>/i)?.[1]?.trim() || ""; return { url, ok: true, title, description }; } catch (error) { return { url, ok: false, error: error.message }; } } ``` ### Technical Analysis The store homepage URL is validated once before the request, after which `fetch` follows redirects automatically. Redirect destinations are not passed through `validateSafeUrl`. The validation also relies on hostname-string checks and does not comprehensively account for IPv6, link-local addresses, non-canonical address representations, DNS rebinding, or a public hostname resolving to a private address. ### Attack Path 1. Shopify context data identifies a merchant-controlled primary storefront domain. 2. The storefront URL passes the initial public-hostname validation. 3. The storefront responds with a redirect to an internal or otherwise blocked destination. 4. The Agent follows the redirect automatically. 5. The Agent reads the destination response into memory and extracts title and description fields. 6. Extracted internal response content can appear in the context JS ...[truncated 704 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/fetch-wechat-article.mjs:217
Finding

Unbounded Buffering of Remote WeChat Content

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/related-product-block.mjs:37
Finding

Unsafe URL Schemes Allowed in Generated Related-Product HTML

Content
View full analysis
${escapeHtml(image.altText || product.title)}` : ""; const priceHtml = price ? `

${escapeHtml(price)}

` : ""; return `

${escapeHtml(heading)}

${imageHtml}

${escapeHtml(product.title)}

${description ? `

${escapeHtml(description)}

` : ""} ${priceHtml}

View ${escapeHtml(product.title)}

`; } ``` ### Technical Analysis `escapeHtml` prevents attribute-delimiter injection but does not make a URL semantically safe. `product.onlineStoreUrl` is accepted without parsing or protocol validation. The fallback `primaryDomain` also accepts any string beginning with `http`, rather than requiring a valid HTTP or HTTPS URL. A crafted product JSON can therefore introdu ...[truncated 1406 chars]
Remediation
View remediation

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/lib/shopify-helpers.mjs:69
Finding

Read-Only Shopify Operations Require Unnecessary Write Scopes

Content
View full analysis
.myshopify.com --scopes read_products,read_content,write_content,read_files,write_files --json ``` ### Technical Analysis The shared GraphQL helper enforces the same complete scope set for every Dev Dashboard operation. This includes read-only commands such as connection checks and context retrieval, even though those commands do not require `write_content` or `write_files`. The CLI onboarding path also requests write scopes before running its read-only connection check. Although write access is legitimately required later for an approved image upload or draft creation, obtaining it during diagnostics and planning exceeds the minimum privileges needed at that stage. The write scopes are relevant to the Skill's eventual declared functionality, so this is not evidence of malicious access. It is nevertheless a concrete least-privilege failure because the authorization lifetime and exposure begin before write access is necessary. ### Attack Path 1. A merchant requests only a connection check, store context, or draft planning. 2. The onboarding flow requests `write_content` and `write_files` in addition to read scopes, or the Dev Dashboard helper refuses to operate without them. 3. The merchant authorizes a ...[truncated 844 chars]
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (10)

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 115)May include surrounding context.

md
3. Load store context with `shopify-context.mjs`.

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · references/onboarding-guide.md (reported line 13)May include surrounding context.

md
### Quick connection (recommended)

Use this for a first run or occasional work. The merchant supplies a store address, never an access token or API secret.

1. Ensure Shopify CLI 3.93.0+ is available.
2. Create private `skill-hub.env` in the merchant's working directory with the skill's `init-env` command. Keep it ignored by Git; never place it in the skill folder.

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · references/onboarding-guide.md (reported line 48)May include surrounding context.

md
### Quick connection (recommended)

Use this for a first run or occasional work. The merchant supplies a store address, never an access token or API secret.

1. Ensure Shopify CLI 3.93.0+ is available.
2. Create private `skill-hub.env` in the merchant's working directory with the skill's `init-env` command. Keep it ignored by Git; never place it in the skill folder.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding

The skill performs network-capable operations against external WeChat URLs and Shopify administrative endpoints, yet it does not declare an explicit tool/permission scope. This creates an authorization and governance gap: an agent runner may permit broader-than-intended access or fail to present meaningful consent boundaries to the user before executing networked actions.

Content

No source excerpt is available for this finding.

Dynamic Request Target

Medium
Category
Server-Side Request Forgery
Confidence
60% confidence
Finding

Request target host is built from a dynamic or untrusted value. If the host is attacker-influenced, this enables SSRF to arbitrary internal or metadata endpoints.

Content

Scanner excerpt · scripts/lib/shopify-dev-dashboard-auth.mjs (reported line 8)May include surrounding context.

js
export function isDevDashboardMode(env = {}) { return (env.SKILL_HUB_SHOPIFY_ACCESS_METHOD || "shopify_cli_oauth") === "dev_dashboard_client_credentials"; }
export function normalizeMyShopifyDomain(value) { const raw = String(value || "").trim(); const match = raw.match(/admin\.shopify\.com\/store\/([^/\s?#]+)/i); const host = match ? `${match[1]}.myshopify.com` : raw.replace(/^https?:\/\//i, "").replace(/\/.*$/, ""); if (!/^[a-z0-9][-a-z0-9]*\.myshopify\.com$/i.test(host)) throw new Error("INVALID_STORE_DOMAIN: Provide a Shopify admin URL or your .myshopify.com address."); return host.toLowerCase(); }
function credentials(env) { const clientId = String(env.SKILL_HUB_SHOPIFY_CLIENT_ID || "").trim(); const clientSecret = String(env.SKILL_HUB_SHOPIFY_CLIENT_SECRET || "").trim(); if (!clientId || !clientSecret) throw new Error("DEV_DASHBOARD_CREDENTIALS_REQUIRED: Add the Client ID and Client Secret to your private configuration, or choose quick browser connection instead."); return { clientId, clientSecret }; }
export async function getDevDashboardAccess(env, shopInput) { const shop = normalizeMyShopifyDomain(shopInput || env.SKILL_HUB_SHOPIFY_STORE_DOMAIN); const { clientId, clientSecret } = credentials(env); const key = `${shop}:${clientId}`; const cached = tokenCache.get(key); if (cached && Date.now() < cached.expiresAt - TOKEN_SKEW_MS) return cached; const response = await fetch(`https://${shop}/admin/oauth/access_token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "client_credentials", client_id: clientId, client_secret: clientSecret }) }); const payload = await response.json().catch(() => ({})); if (!response.ok || !payload.access_token || !payload.expires_in) { const detail = String(payload.error_description || payload.error || ""); const code = /shop_not_permitted/i.test(detail) ? "DEV_DASHBOARD_STORE_NOT_PERMITTED" : (response.status === 401 || response.status === 403 ? "DEV_DASHBO
...[truncated 26 chars]

Dynamic Request Target

Medium
Category
Server-Side Request Forgery
Confidence
60% confidence
Finding

Request target host is built from a dynamic or untrusted value. If the host is attacker-influenced, this enables SSRF to arbitrary internal or metadata endpoints.

Content

Scanner excerpt · scripts/lib/shopify-dev-dashboard-auth.mjs (reported line 10)May include surrounding context.

js
export function isDevDashboardMode(env = {}) { return (env.SKILL_HUB_SHOPIFY_ACCESS_METHOD || "shopify_cli_oauth") === "dev_dashboard_client_credentials"; }
export function normalizeMyShopifyDomain(value) { const raw = String(value || "").trim(); const match = raw.match(/admin\.shopify\.com\/store\/([^/\s?#]+)/i); const host = match ? `${match[1]}.myshopify.com` : raw.replace(/^https?:\/\//i, "").replace(/\/.*$/, ""); if (!/^[a-z0-9][-a-z0-9]*\.myshopify\.com$/i.test(host)) throw new Error("INVALID_STORE_DOMAIN: Provide a Shopify admin URL or your .myshopify.com address."); return host.toLowerCase(); }
function credentials(env) { const clientId = String(env.SKILL_HUB_SHOPIFY_CLIENT_ID || "").trim(); const clientSecret = String(env.SKILL_HUB_SHOPIFY_CLIENT_SECRET || "").trim(); if (!clientId || !clientSecret) throw new Error("DEV_DASHBOARD_CREDENTIALS_REQUIRED: Add the Client ID and Client Secret to your private configuration, or choose quick browser connection instead."); return { clientId, clientSecret }; }
export async function getDevDashboardAccess(env, shopInput) { const shop = normalizeMyShopifyDomain(shopInput || env.SKILL_HUB_SHOPIFY_STORE_DOMAIN); const { clientId, clientSecret } = credentials(env); const key = `${shop}:${clientId}`; const cached = tokenCache.get(key); if (cached && Date.now() < cached.expiresAt - TOKEN_SKEW_MS) return cached; const response = await fetch(`https://${shop}/admin/oauth/access_token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "client_credentials", client_id: clientId, client_secret: clientSecret }) }); const payload = await response.json().catch(() => ({})); if (!response.ok || !payload.access_token || !payload.expires_in) { const detail = String(payload.error_description || payload.error || ""); const code = /shop_not_permitted/i.test(detail) ? "DEV_DASHBOARD_STORE_NOT_PERMITTED" : (response.status === 401 || response.status === 403 ? "DEV_DASHBO
...[truncated 26 chars]

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
76% confidence
Finding

The skill is described as converting a WeChat article into a Shopify blog draft, which obviously requires Shopify API access, but this helper also probes the host environment for npm global roots, APPDATA paths, and executes npm and shopify binaries. That host-level process execution capability is not declared in the stated purpose and is a broader operational capability than directly calling Shopify APIs.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

The manifest describes a skill focused on converting a WeChat article URL into a Shopify blog draft with extraction, image filtering, upload, translation/adaptation, blog selection, and draft creation. This script instead reads an arbitrary local product JSON file and emits a related-product HTML block for embedding, which is a separate merchandising/content-fragment capability not stated in the manifest.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The context command retrieves the full product catalog in paginated batches, including descriptions, collections, media, pricing, and URLs, even though the skill’s stated purpose is converting a WeChat article into a Shopify blog draft. This creates unnecessary data exposure and violates least-privilege/minimization: anyone able to invoke this command with the configured credentials can extract broad store intelligence unrelated to the blog-drafting task.

Content

No source excerpt is available for this finding.

Scope Creep

Low
Category
Excessive Agency
Confidence
75% confidence
Finding

Skill's behavior or capabilities extend beyond its stated purpose. Scope creep allows an agent to perform actions unrelated to its documented functionality, increasing the attack surface.

Content

Scanner excerpt · SKILL.md (reported line 98)May include surrounding context.

md
- Recommend `shopify_cli_oauth` for a quick browser connection.
- Use `dev_dashboard_client_credentials` only when the merchant requests a trusted long-running connection for their own store.
- During Dev Dashboard onboarding, ask whether unattended future permission releases are desired; if yes, configure the optional Automation Token privately. Follow the two-consent upgrade flow in `references/onboarding-guide.md`; never silently broaden scopes.

## User Input

Static analysis

Detected: suspicious.env_credential_access, suspicious.exposed_secret_literal

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
scripts/lib/shopify-dev-dashboard-auth.mjs:4

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
references/onboarding-guide.md:45