T09 · Insecure Skill Coding Practices
- Location
scripts/moysklad.mjs:31- Finding
Arbitrary Credential Forwarding to Attacker-Controlled URLs
- Content
View full analysis
Vulnerability Details
File Location:
scripts/moysklad.mjs, lines 31–48; reachable through lines 267–274
Vulnerability Type: Credential disclosure through unrestricted authenticated network requests
Risk Level: HighVulnerable Code
javascript async function api(method, path, body) { const url = path.startsWith("http") ? path : `${BASE_URL}${path}`; const res = await fetch(url, { method, headers: { Authorization: getAuthHeader(), "Content-Type": "application/json", "Accept-Encoding": "gzip", }, body: body !== undefined ? JSON.stringify(body) : undefined, }); if (res.status === 204) return null; const data = await res.json(); if (!res.ok) { const msg = data?.errors?.[0]?.error ?? res.statusText; throw new Error(`HTTP ${res.status}: ${msg}`); } return data; }The vulnerable function is exposed through the generic API command:
javascript async function cmdApi(positional) { const [method, path, bodyStr] = positional; if (!method || !path) { console.error("Usage: api <METHOD> <path> [json-body]"); process.exit(1); } const body = bodyStr ? JSON.parse(bodyStr) : undefined; const result = await api(method.toUpperCase(), path, body); out(result); }Technical Analysis
The generic
apicommand accepts a user-controlled URL or path. Inapi(), any value beginning withhttpis treated as an absolute URL. The function then unconditionally attaches the MoySkladAuthorizationheader returned bygetAuthHeader().Consequently, the authenticated request is not restricted to the declared MoySklad API origin. An attacker who can influence CLI arguments can direct a request to an arbitrary server and receive either:
- The bearer token from
MOYSKLAD_TOKEN; or - A reusable Basic Authentication value derived from
MOYSKLAD_LOGINandMOYSKLAD_PASSWORD.
...[truncated 2120 chars]
- The bearer token from
- Remediation
View remediation
Remediation Suggestions
-
Reject absolute, scheme-relative, and non-root-relative URLs. The generic command should accept only API paths beginning with
/. -
Resolve the supplied path with the URL parser and validate the resulting destination before attaching credentials:
javascript function buildApiUrl(path) { if (typeof path !== "string" || !path.startsWith("/") || path.startsWith("//")) { throw new Error("Only MoySklad API paths beginning with a single slash are allowed"); } const url = new URL(path, `${BASE_URL}/`); const base = new URL(BASE_URL); if ( url.protocol !== "https:" || url.hostname !== base.hostname || url.port !== "" || !url.pathname.startsWith("/api/remap/1.2/") ) { throw new Error("External API destinations are not allowed"); } return url; } -
Attach the
Authorizationheader only after the final URL has passed the origin and path validation. -
Prefer removing arbitrary HTTP methods from the generic command or explicitly allow only methods required by the requested operation. Require confirmation for destructive methods such as
DELETE. -
Disable automatic redirects where supported, or validate each redirect destination before forwarding authentication. Authentication must never be sent to another origin or downgraded to HTTP.
-
Prefer scoped bearer tokens over reusable login/password credentials and grant the configured account only the minimum MoySklad permissions required.
-
Add regression tests covering external HTTPS URLs, plain HTTP URLs, scheme-relative URLs, deceptive hostnames, alternate ports, encoded paths, and redirect attempts.
-
Revoke and rotate any credential that may previously have been used with an untrusted absolute URL.
-
