T09 · Insecure Skill Coding Practices
- Location
scripts/run.js:59- Finding
Bearer Token Can Be Redirected to an Arbitrary Server
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
The skill is mostly a LinkedIn analysis wrapper, but its bundled runner has broader MCP access and credential handling than the user-facing purpose suggests.
Review before installing. Use this only in a workspace where plaintext .env and .keyapi-cache files are acceptable, avoid entering tokens interactively, set KEYAPI_TOKEN through a safer secret mechanism, do not set KEYAPI_SERVER_URL unless you fully trust the destination, and verify commands always use the intended LinkedIn platform and expected tool names.
scripts/run.js:59Bearer Token Can Be Redirected to an Arbitrary Server
scripts/run.js:258API Token Is Persisted in a Plaintext Project File
scripts/run.js:393LinkedIn and API Responses Are Retained in Plaintext Files
package.json:5Dependency Version Is Not Reproducibly Pinned
The skill is presented as a LinkedIn company analysis tool, but the described runtime behavior can enumerate schemas/tools, invoke generic KeyAPI MCP functionality, target multiple platforms, persist tokens/cache data, and perform unrelated auxiliary processing. That mismatch is dangerous because users may authorize the skill expecting narrow LinkedIn-only behavior while it actually enables broader data access, capability discovery, and persistence than disclosed.
Referenced artifact was not completely inspected
node scripts/run.js --platform linkedin --list-tools
Referenced artifact was not completely inspected
node scripts/run.js --platform linkedin --list-tools
Referenced artifact was not completely inspected
node scripts/run.js --platform linkedin --list-tools
Referenced artifact was not completely inspected
node scripts/run.js --platform linkedin --list-tools
Referenced artifact was not completely inspected
node scripts/run.js --platform linkedin --list-tools
Referenced artifact was not completely inspected
node scripts/run.js --platform linkedin --list-tools
Referenced artifact was not completely inspected
node scripts/run.js --platform linkedin --list-tools
Referenced artifact was not completely inspected
node scripts/run.js --platform linkedin --list-tools
Referenced artifact was not completely inspected
node scripts/run.js --platform linkedin --list-tools
Referenced artifact was not completely inspected
node scripts/run.js --platform linkedin --list-tools
Referenced artifact was not completely inspected
node scripts/run.js --platform linkedin --list-tools
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, "..");
// ── .env loader ───────────────────────────────────────────────────────────────
(function loadDotenv() {
const envPath = join(ROOT, ".env");
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, "..");
// ── .env loader ───────────────────────────────────────────────────────────────
(function loadDotenv() {
const envPath = join(ROOT, ".env");
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, "..");
// ── .env loader ───────────────────────────────────────────────────────────────
(function loadDotenv() {
const envPath = join(ROOT, ".env");
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, "..");
// ── .env loader ───────────────────────────────────────────────────────────────
(function loadDotenv() {
const envPath = join(ROOT, ".env");
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
// ── .env loader ───────────────────────────────────────────────────────────────
(function loadDotenv() {
const envPath = join(ROOT, ".env");
if (!existsSync(envPath)) return;
for (const line of readFileSync(envPath, "utf8").split("\n")) {
const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*)$/);
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
// ── .env loader ───────────────────────────────────────────────────────────────
(function loadDotenv() {
const envPath = join(ROOT, ".env");
if (!existsSync(envPath)) return;
for (const line of readFileSync(envPath, "utf8").split("\n")) {
const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*)$/);
This code persists the entered KEYAPI_TOKEN into a plaintext .env file in the project root, creating a durable local credential artifact. In the context of a distributable skill, that is more dangerous because users may run it inside shared workspaces or repositories where the secret can be discovered, backed up, or committed unintentionally.
reject(new Error("No token entered. Set KEYAPI_TOKEN and try again."));
return;
}
const envPath = join(ROOT, ".env");
writeFileSync(envPath, `KEYAPI_TOKEN=${token}\n`, "utf8");
log(`[token] Saved to ${envPath} — future runs will load it automatically`);
process.env.KEYAPI_TOKEN = token;
The skill requires access to an environment secret (KEYAPI_TOKEN) but does not declare any explicit tool scope or allowed-tools boundary. That weakens least-privilege controls and makes it harder for a host or reviewer to determine what the skill is permitted to access, increasing the chance of unintended secret exposure or broader execution than users expect.
The skill description does not clearly warn that company identifiers, queries, and retrieved LinkedIn data are sent to a third-party service (mcp.keyapi.ai). This creates a data handling and privacy risk because users may provide sensitive research targets or use the skill in regulated environments without understanding that the data leaves the local system.
The skill caches API responses on local disk under .keyapi-cache/ without surfacing that behavior in the user-facing description. Cached responses may contain sensitive company research, employee/job data, or other operational artifacts, which can be exposed to other local users, backups, or later processes if retention and file permissions are not controlled.
This runner is a generic KeyAPI MCP client that can invoke arbitrary server tools with caller-supplied parameters, pagination, schema inspection, and caching, which materially exceeds the declared LinkedIn company analysis scope. In a skill ecosystem, this kind of scope mismatch is dangerous because users and reviewers may trust the manifest while the code actually enables broader data access and capability use.
The CLI accepts an arbitrary --platform value and builds the server URL dynamically, allowing use against unrelated platforms such as TikTok or YouTube despite the skill being presented as LinkedIn company analysis. This expands the attack surface and enables capability drift beyond the user-approved context.
When the token is entered interactively, the script writes it directly to a plaintext .env file for future reuse without an explicit security warning or permission hardening. Persisting credentials in plaintext within the skill directory raises the risk of accidental commit, local theft, or exposure to other processes/users.
No suspicious patterns detected.