T09 · Insecure Skill Coding Practices
- Location
src/convex.ts:18- Finding
Provider OAuth Tokens Are Forwarded to an Unrestricted Backend Origin
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
This appears to be a real Plan2Meal recipe and grocery skill, but it handles OAuth tokens and backend routing in ways users should review carefully before installing.
Install only if you trust the configured Plan2Meal backend and OAuth app operators. Prefer a self-hosted HTTPS CONVEX_URL you control, avoid enabling optional providers you do not need, and treat login as granting the skill temporary access to your provider profile token and Plan2Meal recipe/grocery data. The publisher should validate backend origins, exchange provider tokens for scoped Plan2Meal tokens, bind OAuth state to sessions using cryptographic randomness, restrict recipe URL schemes/targets, add delete confirmation, and pin dependencies.
src/convex.ts:18Provider OAuth Tokens Are Forwarded to an Unrestricted Backend Origin
src/index.ts:330OAuth State Is Not Cryptographically Generated or Bound to the Initiating Session
src/utils.ts:24Recipe URL Validation Permits Unsafe Schemes and Internal Network Targets
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
clawdhub install plan2meal
cp .env.example .env
The declared purpose focuses on recipe and grocery management, but the skill also performs OAuth flows, callback handling, token exchange, profile retrieval, and session/state management. This mismatch can mislead users and reviewers about the sensitivity of the skill, causing them to authorize identity-provider access and backend trust they did not expect.
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
Configure environment:
cp .env.example .env
Environment variables:
The skill declares access to environment-backed capabilities but does not define an explicit tool scope such as permissions or allowed-tools. That weakens least-privilege controls and makes it harder for operators and users to understand what the skill is allowed to access, increasing the chance of unintended secret or backend use.
The deleteRecipe handler performs an irreversible delete via this.convex.deleteRecipe(id) and only reports success after the deletion completes. There is no confirmation prompt, pre-action warning, or comment/docstring disclosing the destructive behavior beyond the command name itself.
The client exposes a getAllRecipes() method that calls recipes.get, which appears broader than the skill’s declared user-scoped behavior and sits alongside a safer getMyRecipes() method. In a chat-driven skill, this creates unnecessary capability for bulk data access if the backend function is not strictly user-authorized, increasing the risk of over-collection or cross-tenant recipe disclosure.
The code reads multiple third-party OAuth client secrets from environment variables and manages external login state locally, even though the skill is presented as a recipe/grocery integration. Handling unrelated OAuth secrets and sessions increases the blast radius of compromise and creates a mismatch between expected functionality and actual privileged behavior.
The skill adds generic multi-provider OAuth flows that are broader than the declared Plan2Meal recipe/grocery scope. This expands the trust boundary to GitHub, Google, and Apple identity systems and creates unnecessary collection and handling of third-party auth artifacts, increasing attack surface and privacy risk if the skill or backend is misconfigured or abused.
The skill exchanges authorization codes and client credentials with external OAuth providers, but users are not clearly informed at the execution point that their data and auth artifacts will be transmitted to GitHub, Google, or Apple. In a chat skill context, hidden or insufficiently explicit external transmission can mislead users about where sensitive authentication data is going.
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
async function fetchUserInfo(provider: AuthProvider, token: string): Promise<UserInfo | null> {
try {
if (provider === 'github') {
const r = await axios.get('https://api.github.com/user', {
headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json' }
});
return {
OAuth access tokens are retained in server memory for up to seven days without explicit disclosure of token retention or session lifetime. Even though storage is in-memory, these tokens can be used to act as the user for the provider-backed session, so undisclosed retention increases privacy and session-hijacking risk.
The runtime dependency axios is specified with a caret range, which permits automatic installation of newer minor/patch releases rather than a single reviewed version. In a skill that likely performs remote API calls, this weakens supply-chain control and can accidentally pull in a vulnerable or behavior-changing release during fresh installs or CI builds.
"author": "",
"license": "MIT",
"dependencies": {
"axios": "^1.6.2"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
Axios has known advisories, and because the manifest uses a broad caret range instead of an exact reviewed version, it is not possible to verify from this file whether installations will resolve to a safe release. In this Plan2Meal skill context, axios is likely used for outbound HTTP requests to APIs or recipe URLs, so unresolved client-side issues could increase exposure to SSRF, request routing bypass, credential leakage, or response tampering depending on how the library is used elsewhere.
The dev dependency @eslint/js is not pinned to an exact version, so development and CI environments may resolve different releases over time. This is primarily a supply-chain integrity and build reproducibility issue rather than an immediate runtime exploit path.
"axios": "^1.6.2"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/node": "^20.10.0",
"eslint": "^9.17.0",
"typescript": "^5.3.0",
The dev dependency @types/node uses a caret range, allowing silent version drift across environments. While this package is not typically shipped to production, unpinned development dependencies still increase supply-chain risk and can affect build consistency.
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/node": "^20.10.0",
"eslint": "^9.17.0",
"typescript": "^5.3.0",
"typescript-eslint": "^8.18.0"
The eslint package is specified as a version range instead of an exact version, which allows CI or local environments to pull different releases over time. This creates low-severity supply-chain and reproducibility risk, especially if development tooling runs with repository access or in privileged CI contexts.
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/node": "^20.10.0",
"eslint": "^9.17.0",
"typescript": "^5.3.0",
"typescript-eslint": "^8.18.0"
}
The typescript compiler is not pinned exactly, so builds may change as newer compatible versions are released. This is mainly a low-risk supply-chain and reliability concern, but compiler changes can alter emitted code or break builds unexpectedly.
"@eslint/js": "^9.17.0",
"@types/node": "^20.10.0",
"eslint": "^9.17.0",
"typescript": "^5.3.0",
"typescript-eslint": "^8.18.0"
}
}
The typescript-eslint dependency is unpinned, allowing environment-dependent resolution of newer releases. As a development tool, the main risk is reduced build reproducibility and potential supply-chain exposure in development or CI rather than direct production compromise.
"@types/node": "^20.10.0",
"eslint": "^9.17.0",
"typescript": "^5.3.0",
"typescript-eslint": "^8.18.0"
}
}
Detected: suspicious.env_credential_access, suspicious.exposed_secret_literal