T09 · Insecure Skill Coding Practices
- Location
scripts/mastodon_scout.py:78- Finding
OAuth Bearer Token Can Be Disclosed to an Arbitrary or Plaintext Endpoint
- Content
View full analysis
Vulnerability Details
File Location:
scripts/mastodon_scout.py:78-84,scripts/mastodon_scout.py:105-116, and credential-bearing call sites throughscripts/mastodon_scout.py:146
Related Documentation:SKILL.md:27andSKILL.md:45-46
Vulnerability Type: Unvalidated credential-bearing network destination
Risk Level: HighVulnerable Code
The API helper attaches the OAuth token to every supplied URL:
python def api_get(url, token): req = urllib.request.Request(url, headers={ 'Authorization': f'Bearer {token}', 'Accept': 'application/json', }) try: with urllib.request.urlopen(req) as resp: return resp.status, resp.read().decode('utf-8') except urllib.error.HTTPError as e: return e.code, e.read().decode('utf-8')The destination is controlled by a command-line option or environment variable without validating its scheme or origin:
python parser.add_argument('--instance', default=os.environ.get('MASTODON_INSTANCE', 'https://mastodon.social')) parser.add_argument('--limit', type=int, default=int(os.environ.get('LIMIT', '20'))) parser.add_argument('--json', action='store_true', dest='raw_json') args = parser.parse_args() token = os.environ.get('MASTODON_TOKEN', '') if not token: print('Error: MASTODON_TOKEN is not set', file=sys.stderr) sys.exit(1) base = args.instance.rstrip('/') if args.command == 'home': status, body = api_get(f'{base}/api/v1/timelines/home?limit={args.limit}', token)Other commands similarly send the token to URLs derived from
base:python elif args.command == 'user-tweets': status, body = api_get(f'{base}/api/v1/accounts/verify_credentials', token) check(status, body) acct_id = json.loads(body)['id'] status, body = api_get(f'{base}/api/v1/accounts/{acct_id}/statuses?limit={args.limit}', token) elif args.command == 'mentions ...[truncated 3772 chars]- Remediation
View remediation
Remediation Suggestions
-
Require HTTPS
- Parse the instance with
urllib.parse.urlsplit. - Reject every scheme other than
https. - Reject missing hostnames, URL fragments, and embedded usernames or passwords.
- Permit HTTP only through an explicit development-only override that never uses production credentials.
- Parse the instance with
-
Bind credentials to an exact origin
- Store each token together with the exact normalized scheme, hostname, and port of the issuing instance.
- Refuse to send a token when the requested origin differs from the token's configured origin.
- Prefer instance-specific variables or configuration records instead of one global token combined with an arbitrary URL.
-
Constrain redirects
- Disable automatic redirects for authenticated API requests or implement a custom redirect handler.
- If redirects are needed, follow them only when the destination has the same validated HTTPS origin.
- Never forward the
Authorizationheader across origins.
-
Validate the instance before constructing API URLs
- Accept only an origin, not arbitrary paths or query strings.
- Normalize internationalized hostnames and ports before comparison.
- Consider an explicit trusted-instance allowlist for automated agent environments.
-
Require informed confirmation for origin changes
- Display the exact normalized destination hostname before first use.
- Require explicit user approval before associating a token with a new instance.
- Do not infer permission to transmit credentials merely because an untrusted prompt supplied
--instance.
-
Apply defense in depth
- Continue recommending minimal read-only OAuth scopes.
- Document that Mastodon tokens are instance-specific and must not be reused across unrelated hosts.
- Avoid logging authorization headers and redact tokens from all errors.
- Provide token revocation and rotation instructions ...[truncated 24 chars]
-
