Back to skill

Security audit

Mail Summary

Security checks for vulnerabilities and agentic risk

Overview

This skill has a coherent Gmail-summary purpose, but it needs review because it stores Google access tokens, prints private email content, writes Calendar events, and starts ongoing token refresh with limited user control.

Install only if you are comfortable giving the skill Gmail read access and Calendar event-write access. Use it in a private environment, protect or delete the stored Google token files, revoke Google OAuth access when finished, avoid enabling the refresh service unless needed, and review Calendar events before allowing automatic creation.

Vulnerability Patterns
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (5)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/setup_auth.py:46
Finding
OAuth credentials and PKCE state are stored without restrictive file permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/setup_auth.py:46-47, 65-75`; `scripts/auth.py:72-74` **Vulnerability Type**: T09: Insecure Skill Coding Practices **Risk Level**: High ### Vulnerable Code ```python with open('.auth_state.json', 'w') as f: json.dump({'code_verifier': code_verifier}, f) ``` ```python if os.path.exists('.auth_state.json'): with open('.auth_state.json') as f: state_data = json.load(f) code_verifier = state_data.get('code_verifier') os.remove('.auth_state.json') try: flow.fetch_token(code=code, code_verifier=code_verifier) token_path = os.path.join(CREDENTIALS_DIR, 'token.json') os.makedirs(CREDENTIALS_DIR, exist_ok=True) with open(token_path, 'w') as f: f.write(flow.credentials.to_json()) ``` The same default-permission write pattern is used when a refreshed token is saved: ```python creds.refresh(Request()) with open(token_path, 'w') as token: token.write(creds.to_json()) ``` ### Technical Analysis The Skill stores a PKCE code verifier and serialized Google OAuth credentials using ordinary `open(..., 'w')` calls. It does not explicitly set the credentials directory to mode `0700` or sensitive files to mode `0600`. The effective permissions therefore depend on the process umask and existing directory permissions. In a permissively configured or shared environment, another local user or process could read: - The Google OAuth access token - The long-lived refresh token - The OAuth client information - The temporary PKCE verifier The refresh token is particularly sensitive because it can be used to obtain new access tokens until the authorization is revoked. The granted scopes are limited to Gmail read-only and Google Calendar event access, but those scopes still expose private correspondence and permit Calendar modification. ### Attack Path 1. The user completes OAuth authorization. 2. `setup_auth.py` writes `token.json` with permissions inherited from the r ...[truncated 942 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Create the credential directory with owner-only permissions: ```python os.makedirs(CREDENTIALS_DIR, mode=0o700, exist_ok=True) os.chmod(CREDENTIALS_DIR, 0o700) ``` 2. Create sensitive files using an explicit owner-only mode: ```python fd = os.open(token_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) with os.fdopen(fd, 'w') as f: f.write(flow.credentials.to_json()) ``` 3. Apply the same protection to `.auth_state.json`, client-secret files, token refresh writes, and any backup or temporary files. 4. Use atomic replacement: write to a protected temporary file, flush and `fsync`, then use `os.replace`. 5. Store OAuth state in the protected credentials directory rather than the current working directory. 6. Delete temporary OAuth state in a `finally` block after completion or failure. 7. Document token revocation procedures and advise users to revoke authorization if credential exposure is suspected. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/setup_auth.py:43
Finding
OAuth callback is accepted without state validation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/setup_auth.py:43-54, 59-78` **Vulnerability Type**: T09: Insecure Skill Coding Practices **Risk Level**: Medium ### Vulnerable Code ```python auth_url, _ = flow.authorization_url( prompt='consent', access_type='offline', code_challenge=code_challenge, code_challenge_method='S256' ) with open('.auth_state.json', 'w') as f: json.dump({'code_verifier': code_verifier}, f) ``` ```python parsed = urllib.parse.urlparse(callback_url) params = urllib.parse.parse_qs(parsed.query) code = params.get('code', [None])[0] if not code: logging.error("No authorization code found in URL.") sys.exit(1) code_verifier = None if os.path.exists('.auth_state.json'): with open('.auth_state.json') as f: state_data = json.load(f) code_verifier = state_data.get('code_verifier') os.remove('.auth_state.json') try: flow.fetch_token(code=code, code_verifier=code_verifier) ``` ### Technical Analysis `flow.authorization_url()` returns an OAuth state value, but the code discards it. The stored authorization-session data contains only the PKCE verifier. When processing the callback, the program extracts the authorization code but neither extracts nor validates the callback’s `state` parameter. PKCE protects an authorization code from being redeemed without the verifier, but it does not replace OAuth state validation. State binds an incoming callback to the authorization flow initiated by the client and mitigates login CSRF and authorization-session confusion. The callback URL is also not checked to ensure that its scheme, hostname, port, and path match the configured redirect URI. Any supplied URL containing a `code` query parameter proceeds to token exchange. ### Attack Path 1. The Skill generates an authorization URL and stores its PKCE verifier. 2. An attacker obtains or is sent that authorization URL. 3. The attacker authorizes the same OAuth client using a Google accou ...[truncated 1069 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Preserve the state returned by `authorization_url()` together with the PKCE verifier: ```python auth_url, state = flow.authorization_url(...) json.dump({ 'state': state, 'code_verifier': code_verifier }, f) ``` 2. Parse the callback state and compare it using `secrets.compare_digest`. 3. Reject callbacks with missing, malformed, expired, or mismatched state. 4. Validate the callback URL against the configured redirect target, including the expected `http` scheme and `localhost` hostname. 5. Associate state with a short creation timestamp and enforce a short expiration period. 6. Delete state after one attempted use to prevent callback replay. 7. Prefer the OAuth library’s supported callback/session-validation interface rather than manually extracting only the authorization code. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/setup_auth.py:80
Finding
OAuth setup synchronously launches an unnecessary perpetual refresh loop<![CDATA[ ## Vulnerability Details **File Location**: `scripts/setup_auth.py:80-106`; `scripts/refresh_service.py:48-66` **Vulnerability Type**: T09: Insecure Skill Coding Practices **Risk Level**: Medium ### Vulnerable Code ```python # Run refresh_service.py automatically after auth complete, only if not already running import subprocess import psutil script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'refresh_service.py') lock_file = os.path.join(os.path.dirname(script_path), 'refresh_service.lock') def is_refresh_running(): if not os.path.exists(lock_file): return False try: with open(lock_file, 'r') as f: pid = int(f.read()) return psutil.pid_exists(pid) except Exception: return False if is_refresh_running(): print("refresh_service.py is already running. Skipping auto-run.") else: try: result = subprocess.run([sys.executable, script_path], check=True, capture_output=True, text=True) print("\n[refresh_service.py output]\n" + result.stdout) except subprocess.CalledProcessError as e: print(f"[refresh_service.py error]: {e.stderr}") ``` The launched process does not terminate normally: ```python try: while True: try: creds = auth_google() # refresh ถ้าจำเป็น logging.info(f"Token refreshed at {datetime.now()}") except Exception as e: logging.error(f"Refresh failed: {e}") time.sleep(REFRESH_INTERVAL) finally: lock.release() ``` ### Technical Analysis `subprocess.run()` is blocking and waits for its child process to exit. The child executes an unconditional `while True` loop, so successful OAuth setup remains blocked indefinitely under normal operation. The background refresh mechanism is also unnecessary for the declared functionality. `auth_google()` already refreshes an expired token on demand before Gmail or Calendar API use. Keeping a process alive continuously therefore exce ...[truncated 1437 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove the automatic refresh-service launch from OAuth setup. 2. Rely on the existing on-demand refresh in `auth_google()`: ```python if creds.expired and creds.refresh_token: creds.refresh(Request()) ``` 3. If a long-running refresh service is genuinely required, make it an explicit opt-in operation. 4. Run it through a properly supervised process manager with start, stop, restart, and resource-limit controls. 5. Do not use blocking `subprocess.run()` for a perpetual service. 6. Use an operating-system lock primitive or verify both PID and process identity to avoid PID-reuse errors. 7. Establish process timeouts and ensure subprocess output is not captured without size limits. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/fetch_emails.py:75
Finding
Private email content and metadata are exposed through console output and logs<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fetch_emails.py:75-81, 101-108` **Vulnerability Type**: T09: Insecure Skill Coding Practices **Risk Level**: Medium ### Vulnerable Code ```python body = clean_text(extract_body(msg_data['payload'])) if not body: body = clean_text(msg_data.get('snippet', '')) if len(body) > MAX_BODY_LENGTH: body = body[:MAX_BODY_LENGTH] + '...' emails.append({'subject': subject, 'from': sender, 'date': date, 'body': body}) logging.info(f"Email: from={sender}, subject={subject}, date={date}") ``` ```python for email in emails: print(f"From: {email['from']}") print(f"Date: {email['date']}") print(f"Subject: {email['subject']}") print(f"Body:\n{email['body']}") print("=" * 60) ``` ### Technical Analysis Processing email content is necessary for the Skill’s declared summarization functionality. However, the implementation prints the sender, date, subject, and up to 500 characters of each message body to standard output. It also logs sender, subject, and date at the default `INFO` level. Standard output and logs generated by agent frameworks, schedulers, process managers, terminals, or CI systems may be retained beyond the requested operation. Consequently, private email data can be copied into storage outside Gmail’s access controls. The metadata log is not required to produce the summary. Full console output is part of the current agent integration, but it should be treated as sensitive data transfer and minimized accordingly. ### Attack Path 1. The Skill accesses Gmail with the user’s authorized read-only token. 2. It fetches up to 50 messages from the configured lookback period. 3. Sender addresses, subjects, dates, and body excerpts are written to logs or standard output. 4. The hosting agent, scheduler, terminal logger, or process manager retains that output. 5. A user, operator, support account, or compromised log collector with access to retained output reads the private corresp ...[truncated 578 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove sender, subject, and date logging from the default `INFO` level. 2. If diagnostic logging is required, log only aggregate counts and timing information. 3. Make message-level diagnostics an explicit, temporary debug option with a clear privacy warning. 4. Avoid writing raw email bodies to persistent logs. 5. Pass email data to the summarization component through a protected in-memory interface where supported. 6. Configure agent and scheduler environments to redact or avoid retaining sensitive command output. 7. Document that email content is processed by the hosting agent and define retention and deletion expectations. 8. Continue using the read-only Gmail scope and retain the existing message-count and body-length bounds. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
scripts/create_event.py:47
Finding
Calendar event creation does not enforce the documented duplicate-event check<![CDATA[ ## Vulnerability Details **File Location**: `scripts/create_event.py:47-53`; `agent/instructions.md:104-108` **Vulnerability Type**: T09: Insecure Skill Coding Practices **Risk Level**: Low ### Vulnerable Code The executable inserts the event directly: ```python try: result = service.events().insert(calendarId='primary', body=event).execute() logging.info(f"Event created: {result.get('htmlLink')}") print(f"Event created: {result.get('htmlLink')}") except HttpError as error: logging.error(f'An error occurred: {error}') ``` However, the agent instructions require a duplicate check: ```text 5. If a meeting or interview is detected in an email: - Check if the event already exists on Google Calendar (e.g., from a Google Calendar invite) - If the event does NOT exist yet, run `create_event.py` to create it - If the event already exists, skip — do not create a duplicate ``` ### Technical Analysis The safety control exists only as a natural-language instruction to the agent. `create_event.py` performs no Calendar query, event-ID comparison, or idempotency check before calling `events().insert()`. Agent reasoning is not a reliable enforcement boundary. Reprocessing an email, retrying after an ambiguous API response, or invoking the script directly can create repeated Calendar events. Email content that causes the model to identify the same meeting more than once can produce the same outcome. ### Attack Path 1. A meeting email is fetched and interpreted by the agent. 2. The agent invokes `create_event.py`, which inserts the event. 3. The same email is processed again during a repeated user request, scheduler run, or retry. 4. The script receives the same title, date, and time. 5. Because no executable duplicate check exists, another event is inserted. 6. Repeated invocations continue creating duplicate entries. ### Impact Assessment An attacker or accidental repeated invocation can affect Calendar integrity by creating duplic ...[truncated 303 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Enforce duplicate detection inside `create_event.py`, not solely in agent instructions. 2. Query Calendar events in the relevant time interval before insertion. 3. Compare normalized title, start time, end time, and timezone. 4. Store a deterministic source identifier, such as a hash of the Gmail message ID and meeting details, in Calendar extended properties. 5. Search for that identifier before creating an event. 6. Use a deterministic Calendar event ID where supported to make retries idempotent. 7. Return a distinct result when an existing event is found. 8. Handle ambiguous API failures by checking whether the event was created before retrying. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (20)

Credential Access

High
Category
Privilege Escalation
Content
]

def find_credentials_file():
    cred_path = os.path.join(CREDENTIALS_DIR, 'credentials.json')
    if os.path.exists(cred_path):
        return cred_path
    matches = glob.glob(os.path.join(CREDENTIALS_DIR, 'client_secret_*.json'))
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
]

def find_credentials_file():
    cred_path = os.path.join(CREDENTIALS_DIR, 'credentials.json')
    if os.path.exists(cred_path):
        return cred_path
    matches = glob.glob(os.path.join(CREDENTIALS_DIR, 'client_secret_*.json'))
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Session Persistence

Medium
Category
Rogue Agent
Content
---
name: mail-summary
description: Fetch Gmail emails from the last 24h, rank by importance, summarize into bullet points, and auto-create Google Calendar events for detected meetings.


# Email & Calendar Assistant Skill
Confidence
84% confidence
Finding
The skill is designed to persist access to Gmail and Google Calendar over time, including auto-refreshing OAuth tokens and automatically creating calendar events based on email content. Persistent delegated access to sensitive communications and calendar data increases the blast radius if the skill, host agent, or stored tokens are compromised, and automatic actions can also create unwanted or manipulated events from malicious emails.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The skill instructs users to send the OAuth client secret JSON and the full localhost redirect URL back through the agent without any explicit warning about the sensitivity of those artifacts or the breadth of Gmail/Calendar access being granted. This can expose OAuth credentials, authorization codes, and account metadata to the agent platform or logs, increasing the risk of account compromise or unintended retention of sensitive access material.

Intent-Code Divergence

Medium
Confidence
94% confidence
Finding
The skill is presented as a mail summarization tool but its normal behavior includes modifying a separate user resource: Google Calendar. This expands capability from read-only summarization to write actions, increasing the chance of surprising side effects, unwanted event creation, and privilege misuse if the user did not clearly consent to calendar changes.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
## Setup Before First Use

> **The agent must run all commands below itself. Do NOT ask the user to run them.**

Before running any script, ensure the Python environment is ready:
Confidence
81% confidence
Finding
The instruction that the agent must run all setup commands itself encourages autonomous execution of environment setup and dependency installation without a user checkpoint. In a skill that later handles OAuth credentials and mailbox/calendar data, this reduces human oversight and can lead to unintended code execution or privilege use in the local environment.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
## Available Commands

> **The agent must run all commands below itself. Do NOT ask the user to run them.**

- `python scripts/fetch_emails.py` — Fetch emails from the last 24 hours and print full content
- `python scripts/create_event.py --title "..." --date "YYYY-MM-DD" --time "HH:MM" [--duration 60]` — Create a Google Calendar event
Confidence
84% confidence
Finding
The autonomous-command instruction applies to operational scripts that read full email contents and create calendar events, allowing the agent to take privacy-sensitive and state-changing actions without an explicit confirmation step. In this context, autonomy increases the risk of overreach, mistaken execution, and silent data access or modification.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The instructions tell the agent to fetch and process full email contents and create calendar events, but they do not require a clear notice or confirmation to the user about accessing sensitive mailbox data and making account changes. In practice, this can expose private content and cause unauthorized modifications under broad OAuth grants.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The setup flow instructs the agent to save client secrets and OAuth tokens locally without warning about their sensitivity or defining storage protections. If these files are left unprotected in the workspace, other local processes, users, or later tasks could reuse them to access the user's Gmail and Calendar.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The background refresh service maintains persistent authenticated access by silently refreshing OAuth tokens every 30 minutes, but the instructions do not clearly warn the user about that ongoing access. This increases the persistence of compromise or misuse if the environment is shared, unattended, or later repurposed.

File System Enumeration

Medium
Category
Data Exfiltration
Content
cred_path = os.path.join(CREDENTIALS_DIR, 'credentials.json')
    if os.path.exists(cred_path):
        return cred_path
    matches = glob.glob(os.path.join(CREDENTIALS_DIR, 'client_secret_*.json'))
    if matches:
        return matches[0]
    raise FileNotFoundError(
Confidence
80% confidence
Finding
Code scans file system directories looking for sensitive files. This could be reconnaissance for credential theft.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The script retrieves mailbox contents, including sender, subject, date, and body, and prints them directly to stdout. In agent or automation environments, stdout is often captured by logs, orchestration systems, chat transcripts, or other downstream consumers, which can unintentionally disclose sensitive email data well beyond the intended user.

Intent-Code Divergence

Medium
Confidence
93% confidence
Finding
The header says the script completes auth and saves token.json, but the implementation also automatically launches refresh_service.py. Undocumented post-auth execution is dangerous because users may grant sensitive OAuth access expecting only token storage, while the script immediately starts additional code with those newly obtained credentials.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The script writes OAuth credentials to token.json on disk without any warning, permission hardening, or user guidance about protecting the file. Stored refresh tokens can provide long-lived access to user data if another local user, backup system, malware, or accidental commit exposes the file.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
print("refresh_service.py is already running. Skipping auto-run.")
        else:
            try:
                result = subprocess.run([sys.executable, script_path], check=True, capture_output=True, text=True)
                print("\n[refresh_service.py output]\n" + result.stdout)
            except subprocess.CalledProcessError as e:
                print(f"[refresh_service.py error]: {e.stderr}")
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Natural-Language Policy Violations

Low
Confidence
81% confidence
Finding
The configuration example sets `timezone: Asia/Bangkok`, which can imply a default locale-specific behavior for calendar event handling. The file does not clearly state that users should choose their own timezone or that the provided value is only an example, which risks a locale policy issue.

Natural-Language Policy Violations

Low
Confidence
81% confidence
Finding
The config sets `timezone: Asia/Bangkok`, which imposes a specific locale/timezone behavior. For a general skill, this can be a natural-language policy concern because it fixes a locale-related setting without any visible opt-in or justification in the file.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Content
MAX_BODY_LENGTH = 500

config = get_config()
LOG_LEVEL = getattr(logging, config.get('log_level', 'INFO').upper(), logging.INFO)
logging.basicConfig(
    level=LOG_LEVEL,
    format='%(asctime)s %(levelname)s %(message)s',
Confidence
50% confidence
Finding
Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Content
MAX_BODY_LENGTH = 500

config = get_config()
LOG_LEVEL = getattr(logging, config.get('log_level', 'INFO').upper(), logging.INFO)
logging.basicConfig(
    level=LOG_LEVEL,
    format='%(asctime)s %(levelname)s %(message)s',
Confidence
50% confidence
Finding
Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Content
MAX_BODY_LENGTH = 500

config = get_config()
LOG_LEVEL = getattr(logging, config.get('log_level', 'INFO').upper(), logging.INFO)
logging.basicConfig(
    level=LOG_LEVEL,
    format='%(asctime)s %(levelname)s %(message)s',
Confidence
50% confidence
Finding
Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Static analysis

No suspicious patterns detected.