T09 · Insecure Skill Coding Practices
- Location
index.js:65- Finding
Shell Command Injection Through Broadcast Arguments
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
This is a disclosed Feishu tenant-wide broadcast tool, but it combines mass messaging authority with unsafe command execution and weak local secret handling.
Install only in a controlled admin environment. Treat it as capable of messaging every Feishu tenant user, reading Feishu app credentials, caching a tenant token locally, and executing local shell commands from provided arguments. Prefer requiring dry-run/confirmation, fixing exec usage, restricting .env loading, and hardening token/temp-file storage before production use.
index.js:65Shell Command Injection Through Broadcast Arguments
index.js:62Predictable and Insecure Temporary Message Files
lib/api.js:57Feishu Tenant Access Token Stored in a Shared Plaintext Cache
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
// Shared Token Cache
const TOKEN_CACHE_FILE = path.resolve(__dirname, '../../../memory/feishu_token.json');
// Robust .env loading
const possibleEnvPaths = [
path.resolve(process.cwd(), '.env'),
path.resolve(__dirname, '../../../.env'),
The code searches for .env files not only in the current working directory but also in parent directories relative to the skill. In a shared agent or multi-project environment, this can unintentionally ingest secrets from unrelated repositories or higher-level directories, expanding the blast radius of any misconfiguration and violating least surprise.
// Robust .env loading
const possibleEnvPaths = [
path.resolve(process.cwd(), '.env'),
path.resolve(__dirname, '../../../.env'),
path.resolve(__dirname, '../../../../.env')
];
Loading .env from ../../../.env broadens secret access beyond the local module boundary and may capture credentials from unrelated contexts. In skill ecosystems where code is reused or nested, this creates an avoidable risk of over-collecting credentials and using secrets the operator did not intend this skill to access.
// Robust .env loading
const possibleEnvPaths = [
path.resolve(process.cwd(), '.env'),
path.resolve(__dirname, '../../../.env'),
path.resolve(__dirname, '../../../../.env')
];
Searching even further up the directory tree for .env files increases the chance of accidental credential capture from parent workspaces or system-level project folders. In the context of an agent skill, this is more dangerous because the skill may run in environments with many adjacent projects and shared secrets, making unintended secret access more likely.
const possibleEnvPaths = [
path.resolve(process.cwd(), '.env'),
path.resolve(__dirname, '../../../.env'),
path.resolve(__dirname, '../../../../.env')
];
let envLoaded = false;
The skill explicitly enables tenant-wide broadcasting to all Feishu users, but the documentation does not present a prominent warning, authorization requirement, approval step, or clear confirmation that the action is organization-wide and potentially disruptive. In the context of enterprise messaging, this can be abused for mass spam, phishing, social engineering, reputational harm, or operational disruption, especially because it supports rich text and media and dynamically targets all users.
The code fetches all users and broadcasts caller-supplied text and images to every account without any confirmation, recipient scoping, or explicit warning about mass transmission. In a messaging-integrated skill, this creates a real risk of accidental bulk disclosure, spam, or misuse of internal recipient data even if the feature is intentional.
The skill builds shell command strings with untrusted values such as targetId, title, and image path, then executes them via child_process.exec. Because exec invokes a shell, crafted input containing shell metacharacters can lead to command injection, and this skill iterates over all users, amplifying the blast radius of any abuse.
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
if (!APP_ID || !APP_SECRET) throw new Error("FEISHU_APP_ID or FEISHU_APP_SECRET not found");
const res = await fetch('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ app_id: APP_ID, app_secret: APP_SECRET })
The code persists a bearer access token to a shared JSON file under a predictable path without setting restrictive file permissions or informing the operator. If other local users, processes, or adjacent skills can read that file, they can reuse the token to access the Feishu tenant APIs until expiry.
The post message content is always constructed under the zh_cn locale key, which forces a specific language/locale behavior. There is no visible user opt-in, alternative locale selection, or documented justification for restricting output to Chinese.
This is a manifest file, so vague-trigger rules apply. The description 'Broadcast messages to all Feishu users in the tenant' defines a very broad activation/use scope without any constraints, exclusions, or specificity about when the skill should be used, which could encourage unintended invocation for high-impact mass messaging.
User-provided message text is written to a predictable local temporary file before sending. This can expose sensitive content to other local processes or leave data behind if execution fails before deletion, making confidentiality dependent on filesystem state and error handling.
The comments repeatedly state an intent to call the existing feishu-post skill via CLI for robustness or safety, but the implemented sendPost path constructs the payload locally and performs the HTTP POST itself. This is an active contradiction between the documented intent in comments and the actual implementation approach.
Using a caret version for dotenv allows automatic adoption of newer minor/patch releases, which can introduce supply-chain risk or unexpected behavior changes if an upstream package is compromised or regresses. While common in Node.js projects, this weakens build reproducibility and increases exposure compared with fully pinned dependencies.
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"dotenv": "^16.3.1",
"node-fetch": "^2.7.0",
"yargs": "^17.7.2"
}
Using a caret version for node-fetch permits non-exact dependency resolution, increasing supply-chain exposure and reducing reproducibility of builds. If an upstream release is malicious or flawed, new installs may pull it in without explicit review.
},
"dependencies": {
"dotenv": "^16.3.1",
"node-fetch": "^2.7.0",
"yargs": "^17.7.2"
}
}
Using a caret version for yargs means future installs may resolve to different package contents than originally tested, creating a low-severity supply-chain and stability risk. This is especially relevant for CLI tooling because argument parsing behavior changes can affect how the skill is invoked.
"dependencies": {
"dotenv": "^16.3.1",
"node-fetch": "^2.7.0",
"yargs": "^17.7.2"
}
}