T09 · Insecure Skill Coding Practices
- Location
scripts/fetch-wechat-article.mjs:211- Finding
Redirect-Based SSRF in WeChat Article and Image Retrieval
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
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.
scripts/fetch-wechat-article.mjs:211Redirect-Based SSRF in WeChat Article and Image Retrieval
scripts/shopify-context.mjs:65Redirect-Based SSRF During Shopify Homepage Metadata Retrieval
scripts/fetch-wechat-article.mjs:217Unbounded Buffering of Remote WeChat Content
scripts/related-product-block.mjs:37Unsafe URL Schemes Allowed in Generated Related-Product HTML
${escapeHtml(price)}
` : ""; return `${escapeHtml(description)}
` : ""} ${priceHtml}scripts/lib/shopify-helpers.mjs:69Read-Only Shopify Operations Require Unnecessary Write Scopes
Referenced artifact was not completely inspected
3. Load store context with `shopify-context.mjs`.
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
### 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.
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
### 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.
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.
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.
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]
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.
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]
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.
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.
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.
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.
- 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
Detected: suspicious.env_credential_access, suspicious.exposed_secret_literal