T09 · Insecure Skill Coding Practices
- Location
get-credential.js:78- Finding
OAuth Polling Token and Credential Object Exposed Through Process Arguments and Standard Output
- Content
View full analysis
Vulnerability Details
File Location:
get-credential.js:78-116,poll-credential.sh:10-27,SKILL.md:127-168
Vulnerability Type: Sensitive authentication material exposure
Risk Level: MediumVulnerable Code
get-credential.js:78-116:javascript async function main() { const args = parseArgs(); if (!args.token) { console.error('❌ Missing --token argument'); console.error('Usage: node get-credential.js --token abc123'); process.exit(1); } try { console.error('🔍 Retrieving OAuth credential...'); const result = await queryConvex('oauth:getToken', { token: args.token }); if (!result.value) { console.error('❌ Token not found or expired'); process.exit(1); } if (!result.value.credential) { console.error('⏳ Authentication not yet completed'); console.error('Make sure the user has clicked the OAuth link and authorized the app.'); process.exit(1); } // Output credential as JSON console.log(JSON.stringify(result.value.credential, null, 2)); } catch (err) { console.error('❌ Failed to retrieve credential:', err.message); process.exit(1); } } main();poll-credential.sh:10-27:bash TOKEN=$1 MAX_ATTEMPTS=${2:-24} # Default: 24 attempts (2 minutes at 5s intervals) if [ -z "$TOKEN" ]; then echo "Error: Missing token argument" >&2 echo "Usage: $0 TOKEN [MAX_ATTEMPTS]" >&2 exit 2 fi for i in $(seq 1 $MAX_ATTEMPTS); do # Try to get credential RESULT=$(node "$(dirname "$0")/get-credential.js" --token "$TOKEN" 2>/dev/null) if [ $? -eq 0 ]; then # Success! Output credential and exit echo "$RESULT" exit 0 fiSKILL.md:127-168also directs the agent to place the token in a command argument and retain the complete credential in a shell variable:bash RESULT=$( ...[truncated 3305 chars]- Remediation
View remediation
Remediation Suggestions
-
Remove secrets from command-line arguments
- Read the polling token from protected standard input.
- Alternatively, use a mode-
0600temporary file or inherited file descriptor. - Avoid placing tokens in environment variables when process environments may be observable.
-
Do not return the complete credential object
- Extract only the fields required for submission, such as a validated username, platform, display handle, and public profile URL.
- Construct a new allowlisted output object instead of serializing the remote response directly.
- Explicitly discard access tokens, refresh tokens, session identifiers, and provider response metadata.
-
Avoid credential propagation through shell variables and stdout
- Replace
CREDENTIAL="$RESULT"with parsing that retains only approved public fields. - Ensure agent tool output and application logs never contain the raw token or credential response.
- Add centralized redaction for token-shaped values and known sensitive property names.
- Replace
-
Harden polling-token lifecycle controls
- Use cryptographically random, single-use polling tokens.
- Bind each token to the initiating session and intended OAuth provider.
- Apply a short expiration and atomically invalidate the token immediately after successful retrieval.
- Prevent repeated retrieval of an already-consumed credential.
-
Validate the remote response
- Enforce a strict schema and reject unexpected credential properties.
- Verify that the returned identity corresponds to the current OAuth session.
- Return a generic failure message without embedding remote response bodies that may contain sensitive data.
-
Update the Skill instructions
- Remove the recommendation to store the full response in
CREDENTIAL. - Instruct the agent to handle only allowlisted public attribution fields.
- Document that OAuth tok ...[truncated 112 chars]
- Remove the recommendation to store the full response in
-
