Back to skill

Security audit

Appian Unnamedobjects

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent Appian audit tool, but its implementation creates review-worthy risks around credential forwarding and local file overwrite.

Review before installing. Use only with a least-privileged Appian API key and a trusted Appian endpoint, and avoid running it from sensitive project directories until the packageZip origin is validated and the saved ZIP filename is sanitized. Expect exported application ZIPs to remain on disk unless manually removed.

Vulnerability Patterns
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (2)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/index.js:113
Finding
API Key Disclosure and Server-Side Request Forgery Through an Unvalidated Download URL<![CDATA[ ## Vulnerability Details **File Location**: `scripts/index.js`, lines 113-120 and 238-241 **Vulnerability Type**: Unvalidated remote URL with credential forwarding **Risk Level**: High ### Vulnerable Code ```js async function downloadZip(credentials, zipUrl) { const res = await fetch(zipUrl, { headers: { 'appian-api-key': credentials.apiKey } }); if (!res.ok) throw new Error(`Download failed [${res.status}]`); const cd = res.headers.get('content-disposition') ?? ''; const fnMatch = cd.match(/filename[^;=\n]*=(['"]?)([^\n"';]+)\1/); const rawName = fnMatch?.[2]?.trim() ?? null; const buf = Buffer.from(await res.arrayBuffer()); return { buf, rawName }; } ``` The remotely supplied URL reaches this function through the deployment polling response: ```js const pollData = await pollExportStatus(credentials, triggerData.uuid); if (!pollData.packageZip) throw new Error('No packageZip URL in response'); const { buf, rawName } = await downloadZip(credentials, pollData.packageZip); ``` ### Technical Analysis The `packageZip` value is obtained from an Appian deployment status response and passed directly to `fetch`. The code does not validate the URL's scheme, hostname, port, embedded credentials, or relationship to the configured `APPIAN_BASE_URL`. More importantly, the `APPIAN_API_KEY` is unconditionally added to the outbound request. If the API response is malicious or compromised, it can direct the Skill to an attacker-controlled host and cause the API key to be disclosed in the `appian-api-key` header. The same behavior creates a server-side request forgery primitive. The response can direct the process to request loopback, link-local, private-network, or other internal destinations accessible from the runtime. Because the entire response is buffered with `arrayBuffer()`, an attacker could also target a large response to consume process memory. ### Attack Path 1. An attacker compromises, impersonates, or otherwi ...[truncated 1417 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse `packageZip` using the standard `URL` class and reject malformed URLs. 2. Require HTTPS for every download destination. 3. Maintain an explicit allowlist of permitted download origins. Prefer requiring the same origin as `APPIAN_BASE_URL`; if Appian legitimately uses separate storage domains, allowlist only the documented exact hosts. 4. Reject URLs containing embedded usernames or passwords, unexpected ports, fragments, loopback addresses, link-local addresses, and private-network destinations unless explicitly required. 5. Do not attach `APPIAN_API_KEY` to arbitrary download URLs. Attach it only after confirming that the destination is a trusted origin that requires this credential. 6. Consider rejecting cross-origin redirects or manually validate every redirect target before following it. 7. Apply a download timeout and maximum response-size limit. Stream the response while enforcing that limit instead of buffering an unrestricted response with `arrayBuffer()`. 8. Use separate, least-privileged credentials for export operations and rotate the existing key if exploitation is suspected. A hardened design should resemble: ```js function validateDownloadUrl(baseUrl, candidate) { const base = new URL(baseUrl); const target = new URL(candidate); if (target.protocol !== 'https:') { throw new Error('ZIP download must use HTTPS'); } if (target.origin !== base.origin) { throw new Error('Untrusted ZIP download origin'); } if (target.username || target.password) { throw new Error('Embedded URL credentials are not permitted'); } return target; } async function downloadZip(credentials, zipUrl) { const target = validateDownloadUrl(credentials.baseUrl, zipUrl); const res = await fetch(target, { redirect: 'error', headers: { 'appian-api-key': credentials.apiKey } }); // Enforce a strict response-size limit before buffering or streaming to ...[truncated 15 chars]

T09 · Insecure Skill Coding Practices

Error
Location
scripts/index.js:124
Finding
Path Traversal and Arbitrary File Overwrite Through an Unsanitized Response Filename<![CDATA[ ## Vulnerability Details **File Location**: `scripts/index.js`, lines 124-140 **Vulnerability Type**: Path traversal in a remotely controlled output filename **Risk Level**: High ### Vulnerable Code ```js function saveZip(buf, rawName, deploymentUuid) { const storagePath = path.join(os.homedir(), 'appian-exports'); const zipName = rawName ? (rawName.endsWith('.zip') ? rawName : `${rawName}.zip`) : `appian-export-${deploymentUuid}.zip`; const outPath = path.join(storagePath, zipName); fs.mkdirSync(storagePath, { recursive: true }); fs.writeFileSync(outPath, buf); process.stderr.write(`ZIP: ${outPath} (${(buf.length / 1024).toFixed(1)} KB)\n`); const cwd = process.cwd(); if (cwd !== storagePath) { const cwdExp = path.join(cwd, 'appian-exports'); fs.mkdirSync(cwdExp, { recursive: true }); fs.copyFileSync(outPath, path.join(cwdExp, zipName)); } } ``` The filename originates from a remote HTTP header: ```js const cd = res.headers.get('content-disposition') ?? ''; const fnMatch = cd.match(/filename[^;=\n]*=(['"]?)([^\n"';]+)\1/); const rawName = fnMatch?.[2]?.trim() ?? null; ``` ### Technical Analysis The Skill treats the filename extracted from the remote `Content-Disposition` header as a trusted local filename. It does not remove directory components, reject `..` segments, or verify that the resolved destination remains under `~/appian-exports`. A filename such as `../../target.zip` is combined with the intended storage directory. Filesystem path normalization can therefore resolve the destination outside that directory. `fs.writeFileSync` then creates or overwrites the resolved file using the downloaded ZIP bytes. The same untrusted filename is reused when copying the archive into the current working directory's `appian-exports` directory. Consequently, there are two path traversal sinks. Exploitation is limited by the operating-system permissions of the Node.js p ...[truncated 1437 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Prefer generating the local archive filename entirely within the Skill, using the validated deployment UUID or a random identifier. Do not trust `Content-Disposition` for filesystem paths. 2. If retaining the server filename is necessary, reduce it to a basename with `path.basename`. 3. Apply a strict filename allowlist, such as ASCII letters, digits, periods, underscores, and hyphens. 4. Reject empty names, `.` and `..`, path separators, control characters, absolute paths, and platform-specific reserved names. 5. Resolve the final path and confirm it remains beneath the intended export directory before every write and copy operation. 6. Use exclusive creation where overwriting is unnecessary, such as the `wx` flag. 7. Apply restrictive file permissions and avoid writing duplicate copies unless required. 8. Use the sanitized filename consistently for both the home-directory write and current-working-directory copy. Example hardening: ```js function safeDestination(directory, remoteName, deploymentUuid) { const fallback = `appian-export-${deploymentUuid}.zip`; const base = remoteName ? path.basename(remoteName) : fallback; const normalized = base.endsWith('.zip') ? base : `${base}.zip`; if (!/^[A-Za-z0-9._-]+\.zip$/.test(normalized)) { throw new Error('Unsafe ZIP filename'); } const root = path.resolve(directory); const destination = path.resolve(root, normalized); if (!destination.startsWith(`${root}${path.sep}`)) { throw new Error('ZIP path escapes export directory'); } return destination; } ``` The safest option is to ignore `rawName` entirely: ```js const zipName = `appian-export-${deploymentUuid}.zip`; ``` ]]>
Vulnerability Patterns
  • 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
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (4)

Lp3

Medium
Category
MCP Least Privilege
Confidence
96% confidence
Finding
The skill declares network and environment-variable capabilities but does not explicitly scope or constrain them with a permissions or allowed-tools policy. That increases the attack surface because a caller or future implementation could access sensitive credentials and external endpoints without a clear least-privilege boundary, making review and enforcement harder.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
---
name: appian-unnamedobjects
description: Find Appian application objects that are missing a description. Exports the application, scans all object XML files, and reports name and UUID for each object with an empty or absent description tag. Use after changes to an application to audit documentation coverage. Trigger phrases — "find Appian objects without descriptions", "which Appian objects are undocumented", "audit Appian missing descriptions", "list undescribed Appian objects", "check Appian documentation coverage". IMPORTANT — credentials (APPIAN_BASE_URL, APPIAN_API_KEY) are already configured in the system; do NOT ask the user for them before running.
metadata:
  clawdbot:
    emoji: "🔎"
Confidence
80% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The user-facing description does not disclose that exported application ZIPs are persisted to disk and may also be mirrored into the current working directory. This can expose potentially sensitive application contents to other local users, later processes, backups, or source-controlled directories, especially in shared or containerized environments.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
## IMPORTANT: credentials are pre-configured

`APPIAN_BASE_URL` and `APPIAN_API_KEY` are already injected by OpenClaw at runtime. **Never ask the user for credentials before running this skill.** Just execute it with the UUID the user provided.

## How users can ask for this
Confidence
80% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Static analysis

Detected: suspicious.env_credential_access

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
scripts/index.js:35