Back to skill

Security audit

EchoSync

Security checks for vulnerabilities and agentic risk

Overview

This skill mostly matches its stated EchoSync trading purpose, but it exposes account tokens and can perform real trading changes with weak safeguards and some under-disclosed behavior.

Review this carefully before installing. Use it only if you are comfortable with an agent that can access EchoSync account data and submit or change Hyperliquid trading actions. Avoid enabling endpoint overrides, do not ask it to print raw tokens unless absolutely necessary, and independently confirm order, leverage, cancellation, and copy-trade settings before allowing commands that affect real funds.

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 (5)

T09 · Insecure Skill Coding Practices

Error
Location
auth.mjs:53
Finding
Configurable API endpoint can expose bearer tokens to an untrusted server<![CDATA[ ## Vulnerability Details **File Location**: `auth.mjs:53-60, 463-475` **Vulnerability Type**: Unvalidated credential-bearing network destination **Risk Level**: High ### Vulnerable Code ```js const API_V2_BASE_URL = process.env.ECHOSYNC_API_V2_URL ?? dotenv.ECHOSYNC_API_V2_URL ?? 'https://go.echosync.io'; ``` ```js async function requestApi(path, token, options = {}) { const baseUrl = resolveApiBaseForPath(path); const url = `${baseUrl}${path}`; printVerboseRequest(url, options); const res = await fetch(url, { ...options, headers: { authorization: `Bearer ${token}`, ...(options.headers ?? {}), }, }); const rawText = await res.text(); ``` ### Technical Analysis The destination for authenticated requests can be overridden through the inherited `ECHOSYNC_API_V2_URL` environment variable or a Skill-local `.env` file. The resulting URL is not restricted to HTTPS and is not checked against an approved hostname before the stored OAuth bearer token is attached. Consequently, a malicious or accidentally unsafe configuration can direct authenticated requests to an attacker-controlled server. This behavior is especially sensitive because the same token authorizes profile, wallet, copy-trading, and Hyperliquid trading operations. Although configurable endpoints can be useful for development, unrestricted endpoint replacement exceeds the minimum privileges required by the production Skill. Authentication credentials should only be sent to an explicitly trusted origin. ### Attack Path 1. An attacker or compromised deployment mechanism sets `ECHOSYNC_API_V2_URL` to an attacker-controlled origin, such as `https://attacker.example`. 2. The user completes EchoSync authentication, causing a bearer token to be stored locally. 3. The user or agent invokes an authenticated command such as `me`, `follows`, or `hl-order`. 4. `requestApi()` constructs the request using the attacker-controlled base URL. 5. The helper attach ...[truncated 771 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Pin credential-bearing production requests to `https://go.echosync.io`. - If endpoint overrides are required for development, enable them only through an explicit development mode that is disabled by default. - Parse the configured value with `new URL()` and enforce: - `https:` protocol. - An exact approved hostname. - An approved port. - No embedded username or password. - Disable automatic cross-origin redirects or verify the origin after every redirect before forwarding authorization headers. - Never attach bearer tokens to an origin that differs from the explicitly approved API origin. - Document any supported endpoint override and warn that it must never reference an untrusted service. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
auth.mjs:269
Finding
OAuth callback accepts unsolicited bearer tokens without state or session binding<![CDATA[ ## Vulnerability Details **File Location**: `auth.mjs:269-290, 345` **Vulnerability Type**: OAuth login CSRF and token substitution **Risk Level**: High ### Vulnerable Code ```js server.on('request', (req, res) => { const url = new URL(req.url, `http://localhost:${port}`); if (url.pathname === '/callback' && req.method === 'GET') { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); const oauthError = url.searchParams.get('error'); const oauthDesc = url.searchParams.get('error_description'); const access_token = url.searchParams.get('access_token'); const expires_in_raw = url.searchParams.get('expires_in'); const hasQuery = [...url.searchParams.keys()].length > 0; if (oauthError) { const detail = oauthDesc?.trim() || oauthError; res.end(callbackErrHtml(detail)); return; } if (access_token) { const expires_in = Number(expires_in_raw ?? '0') || 0; const expires_at = expires_in > 0 ? Math.floor(Date.now() / 1000) + expires_in : null; saveCredentials({ access_token, expires_at, saved_at: Math.floor(Date.now() / 1000), }); ``` ```js const loginUrl = `${OAUTH_WEB_URL}?redirect_uri=${encodeURIComponent( `http://localhost:${port}/callback` )}`; ``` ### Technical Analysis The login flow does not generate or verify an OAuth `state` value. It also does not use an authorization code bound to a PKCE verifier. The callback accepts any non-empty `access_token` query parameter and immediately writes it to the credential file. There is therefore no cryptographic relationship between: - The login initiated by the local helper. - The browser session that authenticated. - The callback received by the local listener. - The token ultimately persisted by the helper. Binding the listener to `127.0.0.1` reduces remote network exposure but does not establish OAuth request integrity. A malicious page, process, browser extension, or part ...[truncated 1531 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Replace implicit bearer-token delivery with OAuth Authorization Code Flow with PKCE. - Generate a cryptographically random `state` value for every login attempt. - Persist the expected state only for the lifetime of the pending login. - Include `state` and the PKCE code challenge in the authorization request. - Require an exact, constant-time state match in the callback. - Exchange the one-time authorization code for tokens through a trusted HTTPS token endpoint. - Reject callbacks containing unsolicited bearer tokens. - Invalidate pending state after the first successful or failed callback. - Consider binding the login attempt to a short-lived local session record containing the callback port, creation time, state, and PKCE verifier. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
callback.html:131
Finding
OAuth relay moves bearer tokens from the URL fragment into the query string<![CDATA[ ## Vulnerability Details **File Location**: `callback.html:131-137` **Vulnerability Type**: Sensitive token exposure through URL query data **Risk Level**: Medium ### Vulnerable Code ```html <script> (function () { const fragment = location.hash.slice(1); if (fragment) { location.replace('/callback?' + fragment); return; } document.getElementById('relay').hidden = true; document.getElementById('bad').hidden = false; })(); </script> ``` The server then reads the promoted value: ```js const access_token = url.searchParams.get('access_token'); ``` ### Technical Analysis OAuth fragments are not ordinarily transmitted in HTTP requests. This page takes the complete fragment and converts it directly into a query string so that the local server can read it. This transformation places a reusable bearer token into request-target data. Query-bearing URLs can be retained or exposed through: - Browser history and browser diagnostics. - Local HTTP instrumentation. - Endpoint security or debugging tools. - Screenshots or copied URLs. - Local proxy and request logging. No third-party resources are loaded by the callback pages, which limits referrer-based leakage. Nevertheless, moving a bearer token into a query parameter is an insecure token transport design and creates avoidable local exposure. ### Attack Path 1. The OAuth service redirects the browser to: `http://localhost:<port>/callback#access_token=<token>&expires_in=...` 2. `callback.html` reads the full fragment. 3. The script navigates to: `/callback?access_token=<token>&expires_in=...` 4. The bearer token becomes part of the browser-visible request URL. 5. A malicious browser extension, local monitoring product, debugging proxy, browser artifact collector, or person with access to browser history obtains the URL. 6. The token is reused until it expires. ### Impact Assessment An exposed bearer token may grant the same privileges as the authenticated use ...[truncated 231 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Use Authorization Code Flow with PKCE so that the browser carries only a short-lived, single-use authorization code. - Exchange the authorization code for tokens through the trusted token endpoint. - Never place access or refresh tokens in URL fragments or query parameters. - After validating the callback, redirect to a token-free success URL. - Set callback responses to prevent caching, for example: - `Cache-Control: no-store` - `Pragma: no-cache` - `Referrer-Policy: no-referrer` - Keep callback pages free of third-party scripts, images, fonts, and analytics. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
auth.mjs:615
Finding
Copy-trade creation silently submits an excessive maximum order size<![CDATA[ ## Vulnerability Details **File Location**: `auth.mjs:615-629` **Vulnerability Type**: Unsafe financial-operation default and least-privilege violation **Risk Level**: High ### Vulnerable Code ```js async function cmdFollowHl(targetWallet, rawArgs) { if (!targetWallet || !ETH_ADDR_REGEX.test(targetWallet)) { throw new Error('Usage: node auth.mjs follow-hl <target_wallet> [key=val …]'); } const token = requireAccessToken(); const me = await requestApi('/api/v2/auth/me', token, { method: 'GET', }); const userInfo = me && typeof me.data === 'object' && me.data ? me.data : {}; const followerWallet = resolveFollowerWallet(userInfo); const payload = { follower_wallet: followerWallet, target_wallet: targetWallet.toLowerCase(), exchange_type: 'hyperliquid', ...parseFollowOptions(rawArgs), max_order_size: '10000000', }; ``` The Skill documentation states that risk and size fields are controlled by backend defaults and policies: ```text Do not allow user input for these fields (backend defaults/policies): min_size, max_size, tp, sl, delay, excluded, allowed, max_leverage. ``` ### Technical Analysis The helper silently sends `max_order_size: '10000000'` when creating every Hyperliquid copy-trade configuration. This is inconsistent with the documented claim that maximum-size controls are left to backend defaults and policies. A ten-million-unit ceiling is materially broader than necessary for ordinary copy-trading setup. It is not derived from the user's balance, risk preference, account limits, or explicit confirmation. Even if the backend ultimately enforces additional limits, the client requests an unnecessarily permissive configuration. For financially sensitive automation, secure defaults should be conservative, transparent, and explicitly confirmed. ### Attack Path 1. A user asks the Skill to follow a Hyperliquid wallet. 2. The agent invokes `follow-hl` with the target wallet and normal options. 3. ...[truncated 795 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove the hardcoded `max_order_size` field and allow documented backend policy to apply. - If a client-provided value is operationally required: - Use a conservative default. - Validate it against account balance and backend limits. - Require explicit user confirmation. - Display the value and its unit before configuration creation. - Enforce a documented upper bound. - Ensure the implementation and `SKILL.md` describe the same risk-control behavior. - Add integration tests asserting that undocumented high-risk limits are not submitted. - Consider requiring confirmation before enabling a new copy-trade configuration. ]]>

other

Warning
Location
auth.mjs:514
Finding
The me command prints the complete raw user profile beyond its documented scope<![CDATA[ ## Vulnerability Details **File Location**: `auth.mjs:514-538` **Vulnerability Type**: Excessive personal-data disclosure **Risk Level**: Medium ### Vulnerable Code ```js async function cmdMe() { const token = requireAccessToken(); const me = await requestApi('/api/v2/auth/me', token, { method: 'GET' }); const userInfo = me && typeof me.data === 'object' && me.data ? me.data : {}; const walletAddresses = extractEthAddresses(userInfo); const defaultWallet = typeof userInfo.default_wallet_address === 'string' ? userInfo.default_wallet_address.toLowerCase() : null; console.log('Current user profile (/api/v2/auth/me):'); if (walletAddresses.length === 0) { console.log(' Wallets: none'); } else { console.log(` Wallets (${walletAddresses.length}):`); for (const addr of walletAddresses) { const suffix = defaultWallet && addr === defaultWallet ? ' (default)' : ''; console.log(` - ${addr}${suffix}`); } } console.log('\nRaw:\n' + JSON.stringify(userInfo, null, 2)); } ``` ### Technical Analysis The documented behavior of `me` is to print detected wallet addresses and identify the default wallet. After producing that filtered output, the implementation also serializes and prints the complete `/api/v2/auth/me` response. The helper cannot guarantee that future or environment-specific API responses contain only wallet addresses. Additional fields could include user identifiers, email addresses, account metadata, provider details, or other personal information. Because the Skill instructs the agent to relay command output, these fields may be copied into chat history, agent context, terminal logs, or observability systems. This violates data minimization: the output includes data not required to fulfill the documented wallet-listing function. ### Attack Path 1. The user asks to view their profile or wallets. 2. The agent invokes `node auth.mjs me`. 3. EchoSync returns a user object contai ...[truncated 689 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove the raw `JSON.stringify(userInfo, null, 2)` output from the default command. - Output only the documented, explicitly selected fields: - Wallet addresses. - Default-wallet marker. - If diagnostic output is necessary, place it behind an explicit option such as `--raw`. - Warn users before printing raw profile data. - Apply an allowlist-based serializer rather than a denylist. - Redact tokens, email addresses, provider identifiers, and other sensitive fields from diagnostic output. - Update the Skill instructions so agents do not relay raw profile objects unless explicitly requested. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • System Prompt LeakageDirect Leakage, Indirect Extraction, Tool-Based Exfiltration
Findings (19)

Credential Access

High
Category
Privilege Escalation
Content
## Credentials

Tokens are stored under `~/.echosync/credentials.json`. Full behavior and command
reference: `SKILL.md`.
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
## Credentials

Tokens are stored under `~/.echosync/credentials.json`. Full behavior and command
reference: `SKILL.md`.
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
| Subcommand                                   | Purpose                                      |
| -------------------------------------------- | -------------------------------------------- |
| `login`                                      | OAuth; writes `~/.echosync/credentials.json` |
| `logout`                                     | Deletes saved credentials                    |
| `status`                                     | Auth state and token expiry                  |
| `token`                                      | Raw `access_token` on stdout (scripting)     |
Confidence
81% confidence
Finding
The skill manages a credential file and exposes a subcommand that prints the raw access token to stdout. Even though the document says not to show the full token unless explicitly requested, any skill that can retrieve and emit bearer tokens increases the risk of credential disclosure through logs, transcripts, or misuse by downstream tooling.

Direct Prompt Extraction

High
Category
System Prompt Leakage
Content
**Link = the URL from `ECHOSYNC_LOGIN_URL`.** The script does **not** open a browser. This skill must adapt without changing user OpenClaw config.

Telegram output rules:

1. Prefer a single Markdown link line: `[Sign in to EchoSync](url)`.
2. Do **not** add the same URL again as plain text in the same message.
Confidence
85% confidence
Finding
Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Missing User Warnings

High
Confidence
98% confidence
Finding
The skill describes a command that can place live market and limit orders but does not require a prominent warning that these actions may execute real trades with real funds. In a trading context, lack of explicit risk disclosure and confirmation materially increases the chance of costly user mistakes.

Credential Access

High
Category
Privilege Escalation
Content
const CALLBACK_OK_HTML_PATH = join(SCRIPT_DIR, 'callback-ok.html');
const CALLBACK_ERR_HTML_PATH = join(SCRIPT_DIR, 'callback-error.html');

// ── .env loading (best-effort, no dependencies) ──────────────────────────────

function loadDotenv() {
  const envPath = join(SCRIPT_DIR, '.env');
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
// ── .env loading (best-effort, no dependencies) ──────────────────────────────

function loadDotenv() {
  const envPath = join(SCRIPT_DIR, '.env');
  if (!existsSync(envPath)) return {};
  const vars = {};
  for (const line of readFileSync(envPath, 'utf8').split('\n')) {
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Missing User Warnings

High
Confidence
98% confidence
Finding
The `token` command prints the raw OAuth access token directly to stdout with no warning, confirmation, or masking. In a CLI/integration context, stdout is commonly captured by shells, logs, process monitors, or other tools, so this can immediately expose a bearer token that grants account access.

Missing User Warnings

High
Confidence
96% confidence
Finding
The `hl-order` command submits live buy/sell orders, which are potentially irreversible financial actions, yet it executes immediately with no confirmation prompt or explicit warning. The help text states that it places an order, but it does not warn about live trading impact or require any extra acknowledgment from the user.

Missing User Warnings

High
Confidence
95% confidence
Finding
The `hl-leverage` command changes leverage settings for a wallet and coin, which can materially affect liquidation risk and account behavior. The operation is sent immediately over the network with no confirmation and no explicit risk disclosure beyond a brief usage string.

Lp3

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding
The skill invokes a local Node.js helper that performs OAuth, reads/writes local credentials, and makes authenticated network requests, but the manifest does not declare an explicit tool scope such as permissions or allowed-tools. That weakens sandboxing and user visibility, making it easier for the skill to access code execution and network capabilities without clear policy boundaries.

Vague Triggers

Medium
Confidence
94% confidence
Finding
Broad trigger phrases like generic login/authentication language can cause the skill to activate during ordinary conversation or when the user meant a different service. In this skill, unintended invocation is more dangerous because activation can start OAuth flows and later enable account-affecting trading operations.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
The logout behavior deletes locally saved credentials, but the user-facing description does not clearly warn about that consequence. This is mainly a safety and usability issue: unexpected credential deletion can disrupt access or confuse users, though it is not typically a direct security compromise.

Vague Triggers

Medium
Confidence
96% confidence
Finding
Trading triggers such as 'buy', 'sell', 'long', and 'short' are extremely generic and can collide with normal finance discussion. Because this skill can place real Hyperliquid orders, an accidental routing or invocation could result in unintended live trades and financial loss.

External Transmission

Medium
Category
Data Exfiltration
Content
const PORT_POLL_MS = 50;
const PORT_POLL_ATTEMPTS = 100; // 5s max wait for child to listen
const ETH_ADDR_REGEX = /^0x[a-fA-F0-9]{40}$/;
const HL_INFO_URL = 'https://api.hyperliquid.xyz/info';
const { cmd, args, verbose: VERBOSE } = parseCliArgs(process.argv.slice(2));

// ── Helpers ───────────────────────────────────────────────────────────────────
Confidence
50% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
Verbose mode logs full request URLs plus parsed query/body data for authenticated API calls. While the Authorization header is not printed, request bodies and query parameters can include wallet addresses, trade details, ids, and other sensitive operational data that may be captured in terminal logs, CI output, or shell history.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The `hl-cancel` command sends a DELETE request to cancel orders immediately, which is a destructive operation affecting the user's trading activity. While the command name implies cancellation, the file lacks any confirmation prompt or stronger warning that it will modify live orders on the connected account.

Intent-Code Divergence

Low
Confidence
98% confidence
Finding
The module header identifies this script as an "echosync.io OAuth CLI helper", but cmdStatus/cmdToken tell users to run "openclaw login" when authentication is missing or expired. This is an active contradiction in inline/user-facing documentation that can mislead operators about what tool they are using.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"format:check": "prettier --check ."
  },
  "devDependencies": {
    "prettier": "^3.4.2"
  }
}
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Static analysis

Detected: suspicious.dangerous_exec, suspicious.env_credential_access, suspicious.exposed_secret_literal

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
auth.mjs:331

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
auth.mjs:54

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
auth.mjs:275