T09 · Insecure Skill Coding Practices
- Location
scripts/index.js:111- Finding
Appian API Key Disclosure and SSRF Through an Unvalidated Download URL
- Content
View full analysis
Vulnerability Details
File Location:
scripts/index.js, lines 111-124 and 174-176
Vulnerability Type: Unvalidated remote URL with credential forwarding
Risk Level: HighVulnerable Code
js async function downloadZip(credentials, zipUrl) { const res = await fetch(zipUrl, { headers: { 'appian-api-key': credentials.apiKey } }); if (!res.ok) { const text = await res.text().catch(() => ''); throw new Error(`Download failed [${res.status}]: ${text}`); } const cd = res.headers.get('content-disposition') ?? ''; const nameMatch = cd.match(/filename[^;=\n]*=(['"]?)([^\n"';]+)\1/); const rawName = nameMatch?.[2]?.trim() ?? null; const buf = Buffer.from(await res.arrayBuffer()); return { buf, rawName }; }js if (!pollData.packageZip) throw new Error(`No packageZip URL in response`); const { buf, rawName } = await downloadZip(credentials, pollData.packageZip);Technical Analysis
The
packageZipvalue originates in the remote deployment-status response and is passed directly tofetch()without validating its protocol, hostname, port, or origin. The request also includes the sensitiveAPPIAN_API_KEYin theappian-api-keyheader.Consequently, a malicious or compromised Appian endpoint can return an attacker-controlled URL and cause the Skill to disclose the API key. The same behavior provides a server-side request forgery primitive because the URL could reference internal services or loopback addresses reachable from the machine running the Skill.
Redirect handling is not explicitly restricted or validated. The implementation therefore does not establish that every credential-bearing request remains on the configured Appian origin. This also conflicts with the documentation's claim that all requests go only to the configured Appian environment.
Attack Path
- An attacker compromises or controls the confi ...[truncated 1341 chars]
- Remediation
View remediation
Remediation Suggestions
- Parse both
APPIAN_BASE_URLandpackageZipwith the standardURLclass. - Require
packageZipto use HTTPS. - Require its origin to exactly match an approved origin. Prefer the origin of
APPIAN_BASE_URLunless Appian documents a separate, fixed download host that can be explicitly allowlisted. - Reject URLs containing unexpected credentials, ports, protocols, or hostnames.
- Do not attach
APPIAN_API_KEYto any cross-origin request. - Disable automatic redirects where supported, or process redirects manually and repeat protocol and origin validation for every redirect target before forwarding credentials.
- Consider rejecting loopback, link-local, private-network, and cloud metadata destinations if cross-origin download hosts must be supported.
- Add tests covering attacker-controlled hosts, protocol-relative URLs, encoded hostnames, redirects, loopback addresses, private addresses, and unexpected ports.
Example validation logic:
js function validateDownloadUrl(baseUrl, zipUrl) { const base = new URL(baseUrl); const target = new URL(zipUrl); if (target.protocol !== 'https:') { throw new Error('The package ZIP URL must use HTTPS'); } if (target.origin !== base.origin) { throw new Error('The package ZIP URL is not on the configured Appian origin'); } return target.href; }The validated URL should be used for the download, and redirect targets must receive equivalent validation.
- Parse both
