Back to skill

Security audit

Google Photos Manager for OpenClaw

Security checks for vulnerabilities and agentic risk

Overview

This Google Photos skill appears intended to do what it says, but it handles OAuth token files unsafely and requests broader Google Photos authority than its commands need.

Review before installing. Use a dedicated Google OAuth client, keep credentials.json and token files in a private directory, never use token files supplied by someone else, and prefer a version that replaces pickle token storage with Google credential JSON plus owner-only file permissions and removes the unused sharing scope.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/gphotos.py:22
Finding
Unsafe Deserialization of OAuth Token Cache Enables Arbitrary Code Execution## Vulnerability Details **File Location**: `scripts/gphotos.py`, lines 22–24 **Vulnerability Type**: Unsafe Python pickle deserialization **Risk Level**: High ```python if os.path.exists(token_path): with open(token_path, 'rb') as token: creds = pickle.load(token) ``` ### Technical Analysis The application deserializes the user-selected token file with `pickle.load()`. Python pickle data is executable: specially constructed objects can invoke arbitrary callables through reduction operations while they are being deserialized. The `token_path` value is controlled through the documented `--token` command-line option. The file is deserialized before the resulting credentials are validated. Therefore, supplying or replacing a token file with a malicious pickle payload can execute arbitrary Python code as soon as any action invokes `get_credentials()`. This issue does not require the malicious file to contain valid Google credentials. ### Attack Path 1. An attacker creates a malicious pickle whose deserialization routine runs a command or Python callable. 2. The attacker convinces the user to use that file as the token cache, or replaces a token file in a writable or shared location. 3. The user invokes a documented command such as: ```bash ./scripts/gphotos.py --action list \ --credentials /path/to/credentials.json \ --token /path/to/malicious-token.pickle ``` 4. `get_credentials()` finds the file and passes it to `pickle.load()`. 5. The embedded payload executes before credential validity is checked. ### Impact Assessment Successful exploitation provides arbitrary code execution with the privileges of the user running the Skill. The attacker could read or alter files accessible to that user, steal Google OAuth credentials and refresh tokens, access other local secrets, execute additional programs, or perform network operations under the user's identity.
Remediation
## Remediation Suggestions Replace pickle with a non-executable serialization format supported by the Google authentication library: ```python if os.path.exists(token_path): creds = Credentials.from_authorized_user_file(token_path, SCOPES) # After obtaining or refreshing credentials: with open(token_path, "w", encoding="utf-8") as token: token.write(creds.to_json()) ``` Additionally: - Validate the parsed credential fields and expected OAuth scopes. - Require the token file to be owned by the current user. - Reject symbolic links and non-regular files. - Store tokens in a private application directory rather than accepting untrusted token files. - If arbitrary token paths remain supported, clearly treat their contents as untrusted input.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/gphotos.py:38
Finding
OAuth Token Cache Is Created Without Enforced Restrictive Permissions## Vulnerability Details **File Location**: `scripts/gphotos.py`, lines 38–39 **Vulnerability Type**: Insecure storage of sensitive authentication tokens **Risk Level**: Medium ```python with open(token_path, 'wb') as token: pickle.dump(creds, token) ``` ### Technical Analysis The token cache is created using ordinary `open()` without explicitly enforcing owner-only permissions. Its resulting permissions depend on the process umask and properties of the selected destination. The serialized Google credentials may contain reusable access and refresh tokens. The destination is also user-selectable through `--token`. Writing to a shared or insufficiently protected directory can expose the resulting file to other local users. The implementation does not verify destination ownership, reject symbolic links, or atomically replace an existing cache. ### Attack Path 1. The Skill is run with a permissive umask or a token path located in a shared or attacker-observable directory. 2. OAuth authorization completes, and the application serializes credentials to the selected path. 3. The resulting file is readable by another local principal, or an attacker manipulates the destination through a pre-existing symbolic link. 4. The attacker copies the stored access or refresh token. 5. The attacker reuses the credentials against Google APIs within the scopes authorized by the user. ### Impact Assessment Exposure of the cache can allow another local user to impersonate the authorized application and access Google Photos capabilities granted to the token. The available scope includes uploading media, reading application-created data, and sharing-related operations. A refresh token may provide continued access until the authorization is revoked.
Remediation
## Remediation Suggestions Store credentials in a private user-specific directory and create the file atomically with mode `0600`. For example, create a temporary file in the same protected directory using `os.open()` with `O_CREAT | O_EXCL` and permission mode `0o600`, write the serialized JSON credentials, and atomically replace the destination. Before reading or writing the cache: - Reject symbolic links and non-regular files. - Verify that the file and parent directory are owned by the current user. - Reject files with group or world permissions. - Use a parent directory with mode `0700`. - Avoid pickle and store credentials using `Credentials.to_json()`. - Document that token files contain sensitive, reusable OAuth credentials.

T05 · Unauthorized Access and Privilege Escalation

Note
Location
scripts/gphotos.py:11
Finding
Unused Google Photos Sharing Scope Violates Least Privilege## Vulnerability Details **File Location**: `scripts/gphotos.py`, lines 11–13 **Vulnerability Type**: Excessive OAuth permissions **Risk Level**: Low ```python SCOPES = ['https://www.googleapis.com/auth/photoslibrary.appendonly', 'https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata', 'https://www.googleapis.com/auth/photoslibrary.sharing'] ``` ### Technical Analysis The implementation provides only the `list`, `create`, and `upload` actions. It does not implement an operation that shares albums or manages sharing state. Nevertheless, every OAuth authorization requests the `photoslibrary.sharing` scope. Although the Skill description broadly mentions sharing images, the reviewed code does not expose that functionality. Requesting this scope therefore exceeds the minimum privileges necessary for the implemented actions and increases the capabilities available to anyone who compromises the token. The flagged `flow.fetch_token(code=code)` operation is itself a necessary part of the OAuth authorization-code exchange and communicates with the provider configured by the user-supplied Google client configuration. The risk is the excessive scope included in that exchange, not the fact that the authorization code is exchanged for tokens. ### Attack Path 1. The user authorizes all scopes requested by the application, including the unused sharing scope. 2. An attacker obtains the stored token through local disclosure, unsafe deserialization exploitation, or another compromise. 3. The attacker submits authorized sharing-related API requests directly to Google, even though the Skill exposes no corresponding command. 4. Google accepts operations permitted by the token's granted scope. ### Impact Assessment A compromised OAuth token has broader Google Photos authority than required by the implemented features. This unnecessarily expands the potential impact of token theft to sharing-related act ...[truncated 159 chars]
Remediation
## Remediation Suggestions Remove the unused scope: ```python SCOPES = [ 'https://www.googleapis.com/auth/photoslibrary.appendonly', 'https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata', ] ``` If sharing functionality is added later, request the sharing scope only when the user invokes that specific operation. Use action-specific scope sets where practical, clearly explain why each permission is required, and require fresh consent when expanding an existing token's permissions.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (19)

Credential Access

High
Category
Privilege Escalation
Content
## Setup

1. **Enable API**: Enable the "Google Photos Library API" in your Google Cloud Console project.
2. **Credentials**: Download your OAuth 2.0 Client ID credentials as `credentials.json`.
3. **Environment**: This skill uses a Python virtual environment located in its folder.

## Usage
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
## Setup

1. **Enable API**: Enable the "Google Photos Library API" in your Google Cloud Console project.
2. **Credentials**: Download your OAuth 2.0 Client ID credentials as `credentials.json`.
3. **Environment**: This skill uses a Python virtual environment located in its folder.

## Usage
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
## Setup

1. **Enable API**: Enable the "Google Photos Library API" in your Google Cloud Console project.
2. **Credentials**: Download your OAuth 2.0 Client ID credentials as `credentials.json`.
3. **Environment**: This skill uses a Python virtual environment located in its folder.

## Usage
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
## Setup

1. **Enable API**: Enable the "Google Photos Library API" in your Google Cloud Console project.
2. **Credentials**: Download your OAuth 2.0 Client ID credentials as `credentials.json`.
3. **Environment**: This skill uses a Python virtual environment located in its folder.

## Usage
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
## Setup

1. **Enable API**: Enable the "Google Photos Library API" in your Google Cloud Console project.
2. **Credentials**: Download your OAuth 2.0 Client ID credentials as `credentials.json`.
3. **Environment**: This skill uses a Python virtual environment located in its folder.

## Usage
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
## Setup

1. **Enable API**: Enable the "Google Photos Library API" in your Google Cloud Console project.
2. **Credentials**: Download your OAuth 2.0 Client ID credentials as `credentials.json`.
3. **Environment**: This skill uses a Python virtual environment located in its folder.

## Usage
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
## Setup

1. **Enable API**: Enable the "Google Photos Library API" in your Google Cloud Console project.
2. **Credentials**: Download your OAuth 2.0 Client ID credentials as `credentials.json`.
3. **Environment**: This skill uses a Python virtual environment located in its folder.

## Usage
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
## Setup

1. **Enable API**: Enable the "Google Photos Library API" in your Google Cloud Console project.
2. **Credentials**: Download your OAuth 2.0 Client ID credentials as `credentials.json`.
3. **Environment**: This skill uses a Python virtual environment located in its folder.

## Usage
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
## Setup

1. **Enable API**: Enable the "Google Photos Library API" in your Google Cloud Console project.
2. **Credentials**: Download your OAuth 2.0 Client ID credentials as `credentials.json`.
3. **Environment**: This skill uses a Python virtual environment located in its folder.

## Usage
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding
The skill documentation indicates network-capable behavior via the Google Photos API, but the manifest does not declare any explicit tool scope such as permissions or allowed-tools. This creates an authorization and review gap: consumers may not understand that the skill can make outbound API calls, and policy enforcement may be bypassed or weakened.

Insecure deserialization: pickle.load()

Medium
Category
Dangerous Code Execution
Content
creds = None
    if os.path.exists(token_path):
        with open(token_path, 'rb') as token:
            creds = pickle.load(token)
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
Confidence
98% confidence
Finding
The script deserializes the OAuth token file using pickle.load(), which can execute arbitrary code if the token file is replaced or tampered with. In a shared workspace or agent environment where file paths may be influenced by other components, this turns local file modification into code execution under the skill's privileges.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The script persists a reusable OAuth access/refresh token to disk without clear user warning or protective controls. In agent or multi-user environments, token files can be copied and reused to access the user's Google Photos data, making the persistence decision materially sensitive.

External Transmission

Medium
Category
Data Exfiltration
Content
'Content-Type': 'application/json'
    }
    payload = {"album": {"title": title}}
    response = requests.post('https://photoslibrary.googleapis.com/v1/albums', headers=headers, json=payload)
    return response.json()

def upload_photo(creds, photo_path, album_id=None):
Confidence
70% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
'Content-Type': 'application/json'
    }
    payload = {"album": {"title": title}}
    response = requests.post('https://photoslibrary.googleapis.com/v1/albums', headers=headers, json=payload)
    return response.json()

def upload_photo(creds, photo_path, album_id=None):
Confidence
70% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
'Content-Type': 'application/json'
    }
    payload = {"album": {"title": title}}
    response = requests.post('https://photoslibrary.googleapis.com/v1/albums', headers=headers, json=payload)
    return response.json()

def upload_photo(creds, photo_path, album_id=None):
Confidence
70% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
'Content-Type': 'application/json'
    }
    payload = {"album": {"title": title}}
    response = requests.post('https://photoslibrary.googleapis.com/v1/albums', headers=headers, json=payload)
    return response.json()

def upload_photo(creds, photo_path, album_id=None):
Confidence
80% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Tainted flow: 'payload' from requests.post (line 80, network input) → requests.post (network output)

Medium
Category
Data Flow
Content
'Content-Type': 'application/json'
    }
    payload = {"album": {"title": title}}
    response = requests.post('https://photoslibrary.googleapis.com/v1/albums', headers=headers, json=payload)
    return response.json()

def upload_photo(creds, photo_path, album_id=None):
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

External Transmission

Medium
Category
Data Exfiltration
Content
if album_id:
        payload['albumId'] = album_id

    response = requests.post('https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate', headers=headers, json=payload)
    return response.json()

if __name__ == '__main__':
Confidence
80% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Tainted flow: 'payload' from requests.post (line 80, network input) → requests.post (network output)

Medium
Category
Data Flow
Content
if album_id:
        payload['albumId'] = album_id

    response = requests.post('https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate', headers=headers, json=payload)
    return response.json()

if __name__ == '__main__':
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Static analysis

No suspicious patterns detected.