Back to skill

Security audit

LinkedIn email + phone enrichment (Apify)

Security checks for vulnerabilities and agentic risk

Overview

This skill does what it says, but it handles personal contact enrichment and an Apify credential in ways users should review before installing.

Install only if you are comfortable sending LinkedIn profile lists to Apify actors and you have a lawful basis to enrich people with email and mobile phone data. Prefer APIFY_TOKEN from a protected environment, avoid --apify-token in shell commands, use a least-privilege Apify token if available, and validate inputs yourself until the skill rejects non-LinkedIn URLs.

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)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/linkedin_email_phone_pipeline.py:119
Finding

Apify API Token Exposed Through URL Query Parameters and Command-Line Arguments

Content
View full analysis
List[Dict[str, Any]]: base_url = f"https://api.apify.com/v2/acts/{actor_id}/run-sync-get-dataset-items" params = {"token": token, "timeout": timeout_sec, "clean": "true"} url = f"{base_url}?{urllib.parse.urlencode(params)}" req = urllib.request.Request( url=url, data=json.dumps(actor_input).encode("utf-8"), headers={"Content-Type": "application/json"}, method="POST", ) ``` The command-line interface also accepts the credential directly: ```python run_cmd = sub.add_parser("run", help="Run combined pipeline") run_cmd.add_argument("--apify-token", help="Apify API token (fallback: APIFY_TOKEN env)") run_cmd.add_argument("--timeout-sec", type=int, default=DEFAULT_TIMEOUT_SEC) run_cmd.add_argument("--input-json", help="Inline JSON payload") run_cmd.add_argument("--input-file", help="Path to JSON payload file") ``` The unsafe command-line option is explicitly documented: ```bash python3 scripts/linkedin_email_phone_pipeline.py run \ --apify-token 'apify_api_xxx' \ --input-file references/sample_input.json ``` ### Technical Analysis The pipeline inserts the Apify API token into the query component of every actor request. HTTPS encrypts the complete request in transit, but it does not prevent credential disclosure through systems that process or record URLs. Query strings may be retained by HTTP diagnostics, application telemetry, reverse proxies, debugging tools, exception reports, or service-side access logs. The alternative `--apify-token` interface creates a separate local exposure channel. Command-line ...[truncated 1974 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Note
Location
scripts/linkedin_email_phone_pipeline.py:43
Finding

Non-LinkedIn URLs Are Accepted and Forwarded to External Apify Actors

Content
View full analysis
str: text = value.strip() if not text: return "" if not re.match(r"^https?://", text, flags=re.IGNORECASE): text = f"https://{text}" text = re.sub(r"^https?://(www\.)?", "https://", text, flags=re.IGNORECASE) text = text.split("?", 1)[0].split("#", 1)[0].rstrip("/") m = re.search(r"linkedin\.com/in/([^/]+)", text, flags=re.IGNORECASE) if m: return f"https://linkedin.com/in/{m.group(1)}" return text ``` The accepted values are then forwarded to the external actors: ```python def run_pipeline(token: str, cfg: Dict[str, Any], timeout_sec: int) -> Dict[str, Any]: phone_rows: List[Dict[str, Any]] = [] email_rows: List[Dict[str, Any]] = [] if cfg["includePhones"]: phone_input = { "linkedinUrls": cfg["linkedinUrls"], "onlyWithPhones": cfg["onlyWithPhones"], } phone_rows = run_actor(token, PHONE_ACTOR_ID, phone_input, timeout_sec) if cfg["includeEmails"]: email_input = { "linkedinUrls": cfg["linkedinUrls"], "includeWorkEmails": cfg["includeWorkEmails"], "includePersonalEmails": cfg["includePersonalEmails"], "onlyWithEmails": cfg["onlyWithEmails"], } email_rows = run_actor(token, EMAIL_ACTOR_ID, email_input, timeout_sec) ``` ### Technical Analysis The declared input contract requires LinkedIn profile URLs, but `normalize_linkedin_url()` does not reject values that fail to match that cont ...[truncated 2509 chars]
Remediation
View remediation
`, and reject empty identifiers or additional unexpected path structures. 5. **Reject unsafe URL components**, including: - User-information components. - Unexpected ports. - Non-allowlisted hostnames. - Control characters. - Invalid percent encoding. - Unexpected query strings and fragments. 6. **Fail closed** instead of returning an unmatched URL: ```python def normalize_linkedin_url(value: str) -> str: text = value.strip() if not text: raise SkillError("LinkedIn URL cannot be empty.") if "://" not in text: text = f"https://{text}" parsed = urllib.parse.urlsplit(text) host = (parsed.hostname or "").lower() if parsed.scheme.lower() != "https": raise SkillError("LinkedIn URLs must use HTTPS.") if host not in {"linkedin.com", "www.linkedin.com"}: raise SkillError("Only LinkedIn profile URLs are accepted.") if parsed.username or parsed.password or parsed.port is not None: raise SkillError("LinkedIn URL contains unsupported authority components.") match = re.fullmatch(r"/in/([^/]+?)/?", parsed.path) if not match: raise SkillError("Expected a LinkedIn /in/ URL.") return f"https://linkedin.com/in/{match.group(1)}" ``` 7. **Add negative tests** for unrelated domains, deceptive subdomains, embedded credentials, private IP addresses, malformed paths, fragments, and non-HTTPS schemes. 8. **Document third-party processing clearly**, stating that ...[truncated 87 chars]
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (6)

Tainted flow: 'req' from os.getenv (line 123, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/linkedin_email_phone_pipeline.py (reported line 130)May include surrounding context.

python
method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=min(timeout_sec + 30, 3600)) as response:
            status_code = getattr(response, "status", 200)
            raw = response.read().decode("utf-8", errors="replace")
    except urllib.error.HTTPError as exc:

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The README promotes enrichment of LinkedIn URLs into emails and mobile phone numbers for lead generation and outbound operations, but provides no meaningful guidance on privacy, consent, lawful basis, or handling of personal data. In this context, the omission increases the risk that users will deploy the skill for non-compliant scraping, profiling, or unsolicited outreach involving sensitive personal contact information.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The skill explicitly sends user-supplied LinkedIn profile URLs to third-party Apify actors for enrichment, but the user-facing description does not clearly warn that this data is being disclosed to an external service. This creates a privacy and consent issue because users may provide personal or sensitive prospect data without understanding it will leave the local workflow and be processed by third parties.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

This code sends input LinkedIn profile URLs and related enrichment parameters to an external Apify API via HTTP POST, which is a privacy-relevant network operation. Although the module docstring states the pipeline uses Apify actors, there is no user-facing warning, confirmation, or explicit disclosure that user-supplied profile data will be transmitted to a third-party service.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/linkedin_email_phone_pipeline.py (reported line 120)May include surrounding context.

python
def run_actor(token: str, actor_id: str, actor_input: Dict[str, Any], timeout_sec: int) -> List[Dict[str, Any]]:
    base_url = f"https://api.apify.com/v2/acts/{actor_id}/run-sync-get-dataset-items"
    params = {"token": token, "timeout": timeout_sec, "clean": "true"}
    url = f"{base_url}?{urllib.parse.urlencode(params)}"
    req = urllib.request.Request(

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
84% confidence
Finding

The script reads an API credential from the APIFY_TOKEN environment variable to authenticate outbound requests. While this is expected for the tool's function, the code does not provide any explicit disclosure or comment warning the user that an environment-stored credential will be consumed for third-party API calls.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.