Back to skill

Security audit

bigin-crm-skill

Security checks for vulnerabilities and agentic risk

Overview

This appears to be a real Bigin CRM integration, but it needs Review because it stores offline CRM tokens, requests broader OAuth access than the code uses, and can change live CRM data in bulk without strong safeguards.

Review before installing for production use. Use a Bigin sandbox first, keep OAuth client secrets and token files out of shared repos and backups, remove unnecessary settings scope if possible, and fix the OAuth callback to use state validation and loopback binding. Treat bulk update, auto-advance, delete, and close-won/close-lost commands as live business-data changes that should be previewed and approved.

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 (2)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/auth.py:55
Finding
OAuth Callback Is Exposed to Login CSRF and Network-Based Code Injection<![CDATA[ ## Vulnerability Details **File Location**: `scripts/auth.py:55-114` **Vulnerability Type**: OAuth login CSRF caused by missing state validation and an unnecessarily exposed callback listener **Risk Level**: Medium ### Vulnerable Code ```python def get_auth_url(self) -> str: """ Generate authorization URL for Bigin Returns: Authorization URL string """ return ( f"https://accounts.zoho.{self.dc}/oauth/v2/auth?" f"scope={self.scope}&" f"client_id={self.client_id}&" f"response_type=code&" f"access_type=offline&" f"redirect_uri={self.redirect_uri}" ) def start_auth_flow(self) -> Dict[str, Any]: """ Start local server and authenticate via browser Returns: Token dictionary with access_token and refresh_token """ auth_code = None class CallbackHandler(http.server.BaseHTTPRequestHandler): def do_GET(handler_self): nonlocal auth_code query = urllib.parse.urlparse(handler_self.path).query params = urllib.parse.parse_qs(query) if 'code' in params: auth_code = params['code'][0] handler_self.send_response(200) handler_self.send_header('Content-type', 'text/html') handler_self.end_headers() handler_self.wfile.write(b""" <html> <body> <h1>Authentication Successful!</h1> <p>You can close this window and return to the terminal.</p> </body> </html> """) else: handler_self.send_response(400) handler_self.end_headers() handler_self.wfile.write(b"Authentication failed. No code received.") def log_message(self, format, *args): # Suppress default logging pass # Open browser for authent ...[truncated 2979 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Generate a cryptographically secure state value for each authorization attempt: ```python import secrets state = secrets.token_urlsafe(32) ``` 2. Include the URL-encoded `state` value in the authorization request and retain the expected value only for the lifetime of the current authentication flow. 3. Require the callback to provide the same state and compare it using `secrets.compare_digest`. 4. Bind the callback listener explicitly to loopback: ```python with socketserver.TCPServer(("127.0.0.1", 8888), CallbackHandler) as httpd: ``` 5. Validate that the parsed callback path is exactly `/callback`. 6. Reject callbacks containing an OAuth `error`, missing or duplicate `code` parameters, missing state, or invalid state. 7. Stop accepting requests immediately after the first valid callback. 8. Add PKCE with an S256 code challenge where supported. 9. Construct authorization parameters with `urllib.parse.urlencode` rather than manual string concatenation. 10. Add automated tests covering invalid state, missing state, an incorrect callback path, duplicate callbacks, and listener binding. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/auth.py:41
Finding
OAuth Scope Includes Unnecessary Full Bigin Settings Access<![CDATA[ ## Vulnerability Details **File Location**: `scripts/auth.py:41-42` **Additional Locations**: `config/oauth-config.json:6-10`, `SKILL.md:258-262`, `README.md:65-69` **Vulnerability Type**: Excessive OAuth permissions and violation of least privilege **Risk Level**: Medium ### Vulnerable Code ```python # Bigin-specific scopes (org.READ required for /bigin/v2/org endpoint) self.scope = "ZohoBigin.modules.ALL,ZohoBigin.settings.ALL,ZohoBigin.org.READ" ``` The same broad settings scope is declared in configuration: ```json "scopes": [ "ZohoBigin.modules.ALL", "ZohoBigin.settings.ALL", "ZohoBigin.org.READ" ] ``` ### Technical Analysis The Skill implements CRM module operations involving pipelines, contacts, companies, tasks, events, and calls. Full module access is therefore related to its declared CRUD and automation functionality. `ZohoBigin.org.READ` supports the implemented `/bigin/v2/org` identity operation. No reviewed implementation accesses a Bigin settings endpoint. Consequently, `ZohoBigin.settings.ALL` is not required by the implemented functionality and exceeds the minimum privileges necessary for the Skill. The authorization flow also requests offline access and stores a refresh token. Offline access is appropriate for automatic token renewal, but it increases the duration and impact of credential compromise. Combining a persistent refresh token with an unnecessary full settings scope expands the available privileges beyond the Skill's legitimate requirements. ### Attack Path 1. The user follows the documented setup and authorizes all requested OAuth scopes. 2. Zoho issues an offline refresh token containing module, organization-read, and full settings permissions. 3. The Skill stores that refresh token in `~/.openclaw/credentials/bigin-crm.json`. 4. A local process, malicious extension, compromised user account, or other attacker capable of reading the token obtains it. 5. The attacker exchanges the refresh token for access ...[truncated 950 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove `ZohoBigin.settings.ALL` from the hardcoded scope string, configuration, and user documentation unless a concrete settings feature is implemented. 2. Request only the narrowest module permissions supported by Zoho for the enabled commands instead of `modules.ALL`, where practical. 3. Keep `ZohoBigin.org.READ` only if the `whoami` operation remains enabled. 4. Separate optional high-privilege features into an explicit secondary authorization flow so users can grant additional scopes only when needed. 5. Ensure the implementation reads the configured scope list from one authoritative source rather than duplicating scopes between code and configuration. 6. Document the purpose and security consequences of every requested scope, especially offline access. 7. After reducing the scopes, require existing users to revoke and reauthorize the application because previously issued refresh tokens may retain the old permissions. 8. Preserve restrictive token-file permissions and consider using the operating system's credential store or keyring for refresh-token protection. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (40)

Credential Access

High
Category
Privilege Escalation
Content
if int(time.time()) - saved_at >= expires_in - 300:
            # Token expired or about to expire, refresh it
            print("Access token expired. Refreshing...")
            refresh_token = tokens.get("refresh_token")
            if not refresh_token:
                raise ValueError("No refresh token available. Please re-authenticate.")
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
Initialize Bigin CRM client
        
        Args:
            auth_token: OAuth2 access token
            dc: Data center - com, eu, in, au, jp, uk, ca, etc.
        """
        self.base_url = f"https://www.zohoapis.{dc}/bigin/v2"
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
Initialize Bigin CRM client
        
        Args:
            auth_token: OAuth2 access token
            dc: Data center - com, eu, in, au, jp, uk, ca, etc.
        """
        self.base_url = f"https://www.zohoapis.{dc}/bigin/v2"
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
Initialize Bigin CRM client
        
        Args:
            auth_token: OAuth2 access token
            dc: Data center - com, eu, in, au, jp, uk, ca, etc.
        """
        self.base_url = f"https://www.zohoapis.{dc}/bigin/v2"
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
Initialize Bigin CRM client
        
        Args:
            auth_token: OAuth2 access token
            dc: Data center - com, eu, in, au, jp, uk, ca, etc.
        """
        self.base_url = f"https://www.zohoapis.{dc}/bigin/v2"
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
Initialize Bigin CRM client
        
        Args:
            auth_token: OAuth2 access token
            dc: Data center - com, eu, in, au, jp, uk, ca, etc.
        """
        self.base_url = f"https://www.zohoapis.{dc}/bigin/v2"
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The README instructs users to place OAuth client credentials, including a client secret, into a local JSON config file without guidance on secret handling, exclusion from source control, or secure storage. This increases the risk of credential leakage through git commits, backups, shared workstations, or artifact packaging, which could enable unauthorized API access if the credentials are exposed.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The usage examples include create, update, win/lose, advance, and bulk-update commands that modify live CRM records, but the markdown does not warn users that these actions change production data or recommend using a sandbox first during operation. Under the markdown-file criteria, descriptions should disclose behaviors that can affect user data or system integrity.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The file advertises CSV contact import and automation commands that can create tasks, reassign owners, or auto-advance pipelines, but it omits cautionary language about bulk updates and automated actions affecting many records at once. This is a markdown-level missing warning because the described behavior could materially affect user data and workflow integrity.

Session Persistence

Medium
Category
Rogue Agent
Content
### 2. Pipeline Management (Core Feature)
```bash
# Create a pipeline entry (like a deal/opportunity)
bigin pipeline create --contact-id 12345 --company-id 67890 \
  --stage "Initial Contact" --amount 50000 \
  --closing-date "2026-03-15" --owner "sales@yourcompany.com"
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The skill promotes bulk-update and auto-advance automation that can change many pipeline records at once, including moving deals to 'Closed Won', without warning about reversibility, review, or safeguards. In a sales CRM context, such actions can corrupt business data, trigger downstream workflows, distort forecasting, and be difficult to unwind at scale.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The skill documents an automated workflow that creates or updates CRM contacts, companies, pipelines, tasks, and even sends replies based on incoming email, but it does not mention confirmation prompts, approval gates, or disclosure that external CRM state will be modified. This can lead to unintended data creation, incorrect record linkage, privacy issues, and unauthorized outbound actions if email content is misparsed or attacker-controlled.

Natural-Language Policy Violations

Medium
Confidence
92% confidence
Finding
The config sets `default_dc` to `com`, which corresponds to the United States, while also defining multiple regional data centers. This creates a natural-language locale policy concern because the skill defaults users to a specific region without indicating user selection, opt-in, or a documented justification for that locale choice.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
This code persists access and refresh tokens to a file under the user's home directory, which is a safety-relevant credential storage operation. Although permissions are restricted and comments describe the implementation, there is no user-facing disclosure at the point of action explaining that sensitive tokens will be saved locally.

External Transmission

Medium
Category
Data Exfiltration
Content
"""
        url = f"{self.base_url}/Pipelines"
        payload = {"data": [data]}
        response = requests.post(url, headers=self.headers, json=payload)
        response.raise_for_status()
        return response.json()
Confidence
80% 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
"""
        url = f"{self.base_url}/Pipelines"
        payload = {"data": [data]}
        response = requests.post(url, headers=self.headers, json=payload)
        response.raise_for_status()
        return response.json()
Confidence
80% 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
"""
        url = f"{self.base_url}/Pipelines"
        payload = {"data": [data]}
        response = requests.post(url, headers=self.headers, json=payload)
        response.raise_for_status()
        return response.json()
Confidence
80% 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
"""
        url = f"{self.base_url}/Pipelines"
        payload = {"data": [data]}
        response = requests.post(url, headers=self.headers, json=payload)
        response.raise_for_status()
        return response.json()
Confidence
80% 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
"""
        url = f"{self.base_url}/Pipelines"
        payload = {"data": [data]}
        response = requests.post(url, headers=self.headers, json=payload)
        response.raise_for_status()
        return response.json()
Confidence
80% 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
"""
        url = f"{self.base_url}/Pipelines"
        payload = {"data": [data]}
        response = requests.post(url, headers=self.headers, json=payload)
        response.raise_for_status()
        return response.json()
Confidence
80% 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
"""
        url = f"{self.base_url}/Pipelines"
        payload = {"data": [data]}
        response = requests.post(url, headers=self.headers, json=payload)
        response.raise_for_status()
        return response.json()
Confidence
80% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
When DEBUG is enabled, the client logs full request headers and response bodies to stderr, which includes the OAuth Authorization header and potentially sensitive CRM data. In an agent or shared execution environment, stderr may be captured by logs, exposing credentials and customer records to operators, other tools, or downstream log systems.

External Transmission

Medium
Category
Data Exfiltration
Content
"""
        url = f"{self.base_url}/Pipelines/{pipeline_id}"
        payload = {"data": [data]}
        response = requests.put(url, headers=self.headers, json=payload)
        response.raise_for_status()
        return response.json()
Confidence
80% 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
"""
        url = f"{self.base_url}/Pipelines/{pipeline_id}"
        payload = {"data": [data]}
        response = requests.put(url, headers=self.headers, json=payload)
        response.raise_for_status()
        return response.json()
Confidence
80% 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
"""
        url = f"{self.base_url}/Pipelines/{pipeline_id}"
        payload = {"data": [data]}
        response = requests.put(url, headers=self.headers, json=payload)
        response.raise_for_status()
        return response.json()
Confidence
80% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Static analysis

No suspicious patterns detected.