Back to skill

Security audit

Linkedin Odoo

Security checks for vulnerabilities and agentic risk

Overview

The skill does what it says, but it automatically sends contact details to DuckDuckGo and writes an unverified search result into Odoo CRM records.

Install only if you are comfortable letting the skill use your Odoo credentials to update contact records and send each selected contact's name and company to DuckDuckGo. Review the found LinkedIn URL before saving it, and avoid using this unchanged in environments with strict CRM data integrity, privacy, or third-party processing requirements.

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

other

Warning
Location
scripts/update_linkedin.py:69
Finding
Odoo Contact PII Disclosed to an External Search Provider<![CDATA[ ## Vulnerability Details **File Location**: `scripts/update_linkedin.py`, lines 8–11 and 69–73 **Vulnerability Type**: External disclosure of personal data **Risk Level**: Medium ### Complete Code Snippet ```python def get_linkedin_url(query): url = 'https://html.duckduckgo.com/html/' data = urllib.parse.urlencode({'q': query}).encode('utf-8') req = urllib.request.Request(url, data=data, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}) try: html = urllib.request.urlopen(req).read().decode('utf-8') ``` ```python name = partner.get('name', '') company_name = '' if partner.get('parent_id'): # parent_id is usually a list: [id, display_name] company_name = partner['parent_id'][1] search_query = f"site:linkedin.com/in {name} {company_name}".strip() print(f"Searching for: {search_query}") linkedin_url = get_linkedin_url(search_query) ``` ### Technical Analysis The script reads a contact's name and associated company from Odoo, embeds both values in a search query, and transmits that query to `html.duckduckgo.com`. These values can constitute personal or commercially sensitive information. The external search is documented in `SKILL.md` and supports the Skill's declared purpose, so there is no evidence of covert credential exfiltration. Odoo credentials are not included in the DuckDuckGo request. However, the script performs the disclosure automatically without an explicit confirmation step, configurable privacy policy, or data-minimization control. The disclosure is necessary only for the selected public-search implementation, not inherently for updating an Odoo field. Deployments subject to privacy, confidentiality, data-residency, or third-party processing restrictions may therefore consider this behavior excessive. ### Attack Path 1. A user or agent invokes the script with an Odoo contact ID. 2. The script authenticates to Odoo using credentials from environment variables. 3. It reads the con ...[truncated 766 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Require explicit user confirmation before transmitting Odoo contact data to an external search provider. 2. Clearly identify the destination and the exact fields that will be disclosed. 3. Minimize submitted data; omit the company name unless it is needed to disambiguate the contact. 4. Support an organization-approved or privately hosted search service. 5. Add a configuration option that disables external lookup and permits a user-supplied LinkedIn URL instead. 6. Document applicable retention, privacy, and data-residency considerations. 7. Avoid logging the complete query when logs may be accessible to parties that should not receive contact data. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/update_linkedin.py:14
Finding
Insufficient Validation of Search-Derived URL Before Odoo Write<![CDATA[ ## Vulnerability Details **File Location**: `scripts/update_linkedin.py`, lines 14–23 and 76–83 **Vulnerability Type**: Untrusted URL validation failure **Risk Level**: Medium ### Complete Code Snippet ```python # Search for linkedin.com/in/ links in the raw duckduckgo HTML match = re.search(r'href=[\'"]([^\'"]*linkedin\.com/in/[^\'"]*)[\'"]', html) if match: url_str = match.group(1) # Remove any URL encoding DuckDuckGo might have wrapped around it url_str = urllib.parse.unquote(url_str) if 'uddg=' in url_str: url_str = url_str.split('uddg=')[1].split('&')[0] return urllib.parse.unquote(url_str) ``` ```python linkedin_url = get_linkedin_url(search_query) if linkedin_url: print(f"Found LinkedIn URL: {linkedin_url}") models.execute_kw( db, uid, password, 'res.partner', 'write', [[partner_id], {'x_linkedin_url': linkedin_url}] ) print("Successfully updated partner in Odoo.") ``` The script also proceeds after detecting an existing value: ```python if partner.get('x_linkedin_url'): print(f"Partner already has a LinkedIn URL: {partner['x_linkedin_url']}") # Allow overwrite? Let's just update it anyway or skip? We'll proceed. ``` ### Technical Analysis The validation logic searches an untrusted HTML response for any `href` value containing the substring `linkedin.com/in/`. It does not parse the resulting value as a URL or verify its scheme, hostname, port, user-information component, or normalized path. A URL such as the following can satisfy the substring check while directing the user to a non-LinkedIn host: ```text https://attacker.example/redirect?target=linkedin.com/in/example ``` The parser also takes the first matching result and decodes it multiple times. Search-result wrappers and encoded delimiters are handled through string splitting rather than structured query parsing. This makes the trust decision dependent on externally supplied HTML and brittle normalizati ...[truncated 1522 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse candidates with `urllib.parse.urlparse` instead of validating them through substring matching. 2. Require the scheme to be exactly `https`. 3. Normalize the hostname and require it to be exactly `linkedin.com` or `www.linkedin.com`. 4. Require the normalized path to begin with `/in/` and contain a nonempty profile identifier. 5. Reject URLs containing user-information, unexpected ports, malformed percent encoding, or external redirect wrappers. 6. Parse DuckDuckGo wrapper parameters with `urllib.parse.parse_qs` rather than splitting strings on `uddg=` and `&`. 7. Validate again after every decoding or normalization operation. 8. Present the selected profile to the user for confirmation before writing it to Odoo. 9. Do not overwrite a nonempty `x_linkedin_url` unless the user explicitly authorizes replacement. 10. Consider collecting several candidates and matching additional profile attributes instead of trusting the first regular-expression match. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (3)

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill advertises and relies on environment-variable access and outbound network access, but it does not declare any explicit tool scope or permissions boundary. That makes the capability set implicit rather than reviewable, increasing the chance of over-privileged execution or unexpected secret/network use when the skill is invoked.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The skill updates `res_partner.x_linkedin_url` in Odoo, but the description does not prominently warn that it will modify CRM data. Users may invoke it expecting a lookup-only action, leading to unintended writes, bad profile matches, or silent data integrity issues in business records.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The script builds a search query from an Odoo contact's name and company and sends it to DuckDuckGo, which discloses personal/business contact data to a third-party service without explicit consent, notice, or approval controls. In this skill's context, that external transmission is core behavior, so the privacy risk is real and not merely theoretical, especially in regulated or confidentiality-sensitive environments.

Static analysis

No suspicious patterns detected.