Back to skill

Security audit

LoomLens Live

Security checks for vulnerabilities and agentic risk

Overview

LoomLens matches its stated cost-estimation purpose, but it needs review because it stores an API key in browser storage and exposes weakly scoped prompt and model-routing paths.

Review this skill before installing in a workspace with sensitive prompts or paid model access. Use a limited Signal Loom API key if possible, avoid entering secrets into prompts sent through the API estimate flow, and install only where the OpenClaw route and sidebar can be isolated from untrusted pages or callers.

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)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
loomlens-openclaw-plugin.ts:137
Finding

Unauthenticated and Persistent Model Override

Content
View full analysis
{ // event.context contains: sessionKey, messages, preferredModel, modelHint, cfg // If loomlens has a model override pending for this session, apply it here. const override = await getPendingModelOverride(event.context.sessionKey); if (override) { event.model = override; } return event; }; ``` ```ts const pendingOverrides = new Map(); async function setPendingModelOverride(sessionKey: string, modelId: string): Promise { pendingOverrides.set(sessionKey, modelId); } async function getPendingModelOverride(sessionKey: string): Promise { return pendingOverrides.get(sessionKey); } ``` ```ts api.registerRoute('POST', '/loomlens/override', async (req, res) => { const { sessionKey, modelId } = req.body ?? {}; if (!sessionKey || !modelId) { res.status(400).json({ error: 'sessionKey and modelId required' }); return; } await setPendingModelOverride(sessionKey, modelId); res.json({ ok: true, modelId }); }); ``` ### Technical Analysis The model-override endpoint accepts a caller-supplied session key and model identifier without performing authentication or verifying that the caller owns the specified session. The supplied model identifier is not validated against an approved model registry. The override is subsequently trusted by the `before_model_resolve` hook and assigned directly to `event.model`. Although the documented behavior describes an override for the next prompt, `getPendingModelOverride` does not remove the value after use. There is also no expiry timer for overrides. Consequently, a successfully planted override remains active for subsequen ...[truncated 1708 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
loomlens-sidebar.html:933
Finding

Unrestricted Cross-Origin Prompt Messaging

Content
View full analysis
{ if (e.data?.type === 'openclaw_prompt' && e.data?.text) { $('ll_prompt_ta').value = e.data.text; $('ll_drop_hint').classList.add('hidden'); window.LL.estimate(); } }); ``` ### Technical Analysis Outbound `postMessage` calls use `"*"` as the target origin while transmitting the complete prompt and selected model. Any parent page embedding the sidebar can therefore receive this data, regardless of whether it is an authorized OpenClaw origin. The inbound message handler checks only the shape of `e.data`. It does not verify `e.origin` or confirm that `e.source` is the expected parent window. Any window capable of sending messages to the sidebar can inject an `openclaw_prompt` event and alter the prompt used for estimation. These two trust failures create both a confidentiality issue and a cross-origin state-manipulation issue. The same vulnerable implementation is present in the deployable `dist/loomlens-sidebar.html` artifact. ### Attack Path #### Outbound Prompt Disclosure 1. An attacker hosts a page that embeds the sidebar in an iframe. 2. The victim enters or imports a sensitive prompt and dispatches it through LoomLens. 3. The sidebar sends a `loomlens_run` message to its parent with the wildcard target origin. 4. The attacker's parent page receives the c ...[truncated 1128 chars]
Remediation
View remediation
{ if (event.origin !== TRUSTED_ORIGIN) return; if (event.source !== window.parent) return; const data = event.data; if ( data?.type !== 'openclaw_prompt' || typeof data.text !== 'string' || data.text.length > MAX_PROMPT_LENGTH ) { return; } // Process the validated message. }); ``` 3. Define a strict message schema and reject unexpected properties, types, and excessive payload sizes. 4. Use a per-session nonce or handshake so that origin validation is not the only message-authentication control. 5. Add a Content Security Policy with a restrictive `frame-ancestors` directive to prevent untrusted sites from embedding the sidebar. 6. Avoid transmitting full prompt contents when only a model identifier or opaque request reference is required. 7. Apply the correction to both source and generated `dist/` artifacts, then regenerate and verify the deployment package. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
loomlens-sidebar.html:441
Finding

Signal Loom API Key Persisted in Plaintext Browser Storage

Content
View full analysis
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (18)

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The skill stores the user's API key in localStorage, which is accessible to any script running in the same origin, including compromised components or future XSS. Because there is no visible disclosure or safer storage approach, users may unknowingly persist a sensitive credential in a location with broad script access.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The mapper comment says it 'maps' the API response into the local result format and injects API costs, but the implementation references local.allModels, modelId, inputTokens, totalCost, cr.models, and compositeScore, while the local estimator returns all, id, inTokens, cost, sorted, and score. This is an active contradiction between the documented intent of the mapper and what the code can actually operate on.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
98% confidence
Finding

The inline comment at L0698 states the free counter resets 'at midnight UTC', but the implementation sets freeReset = now + 86400, which is simply 24 hours from the current timestamp. That is a direct contradiction in intent/documentation rather than a minor omission, because it affects how quota behavior is presented to users.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
78% confidence
Finding

This is an intentional external network transmission to a third-party API, carrying the user's prompt and bearer API key. In the context of an agent skill, that is security-relevant because prompts may include sensitive data and the transmission occurs to an external service, making confidentiality dependent on user awareness, trust boundaries, and the remote service's handling.

Content

Scanner excerpt · dist/loomlens-sidebar.html (reported line 1022)May include surrounding context.

html
target_models: [modelId],
          include_postmortem: false,
        };
        const res = await fetch('https://api.signallloomai.com/v1/estimate', {
          method: 'POST',
          headers: {
            'Authorization': 'Bearer ' + state.apiKey,

External Transmission

Medium
Category
Data Exfiltration
Confidence
78% confidence
Finding

This is an intentional external network transmission to a third-party API, carrying the user's prompt and bearer API key. In the context of an agent skill, that is security-relevant because prompts may include sensitive data and the transmission occurs to an external service, making confidentiality dependent on user awareness, trust boundaries, and the remote service's handling.

Content

Scanner excerpt · dist/loomlens-sidebar.html (reported line 1022)May include surrounding context.

html
target_models: [modelId],
          include_postmortem: false,
        };
        const res = await fetch('https://api.signallloomai.com/v1/estimate', {
          method: 'POST',
          headers: {
            'Authorization': 'Bearer ' + state.apiKey,

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The code sends the full prompt text to api.signallloomai.com for estimation, but the UI does not present a clear, specific warning that user-entered content will leave the local context. In a sidebar/assistant setting, prompts can contain sensitive code, documents, or secrets, so silent transmission increases privacy and confidentiality risk.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The plugin presents itself as a cost-visibility tool, but it also actively overrides model selection through a hook and an unauthenticated override pathway. That mismatch is security-relevant because operators may grant it access assuming it is read-only, while it can change execution behavior and potentially route prompts to a different model than the user intended.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The manifest declares only a GET estimate route, but the implementation also exposes a POST /loomlens/override endpoint that changes runtime behavior. Undeclared privileged capabilities reduce auditability and can evade administrative review, making it easier for a plugin to introduce hidden control paths.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The plugin captures prompt content from the pre-build hook and retains it per session without any visible disclosure, consent mechanism, or access control context. Prompt bodies often contain secrets, internal data, or personal information, so silent collection increases privacy and confidentiality risk even if the current route shown estimates only supplied query text.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The component stores the user's API key in localStorage, which is readable by any script running in the same origin and persists across sessions. In a browser-integrated skill context, this increases the blast radius of any XSS, compromised dependency, or same-origin component by exposing long-lived credentials.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
87% confidence
Finding

This code performs an outbound network request to a third-party API and includes both the user's prompt and bearer API key. External transmission is expected for this feature, but in a security review of an agent skill it is still a real data-exfiltration surface because any sensitive prompt contents leave the local environment.

Content

Scanner excerpt · loomlens-sidebar.html (reported line 1022)May include surrounding context.

html
target_models: [modelId],
          include_postmortem: false,
        };
        const res = await fetch('https://api.signallloomai.com/v1/estimate', {
          method: 'POST',
          headers: {
            'Authorization': 'Bearer ' + state.apiKey,

External Transmission

Medium
Category
Data Exfiltration
Confidence
87% confidence
Finding

This code performs an outbound network request to a third-party API and includes both the user's prompt and bearer API key. External transmission is expected for this feature, but in a security review of an agent skill it is still a real data-exfiltration surface because any sensitive prompt contents leave the local environment.

Content

Scanner excerpt · loomlens-sidebar.html (reported line 1022)May include surrounding context.

html
target_models: [modelId],
          include_postmortem: false,
        };
        const res = await fetch('https://api.signallloomai.com/v1/estimate', {
          method: 'POST',
          headers: {
            'Authorization': 'Bearer ' + state.apiKey,

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

User prompt content is sent to an external service along with an authorization bearer token, but the UI does not provide clear, explicit disclosure at the point of transmission. In an agent skill, prompts may contain sensitive data, so undisclosed third-party transmission creates a privacy and data-handling risk even if the destination is expected.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
81% confidence
Finding

The documentation at L314-L319 characterizes the engine as standalone and making no API calls or using dependencies, which is true, but it also omits and implicitly downplays that the file mutates global browser state by assigning to window.LoomLensClusters at L299-L310. Because the comment presents the module as a self-contained estimator while the code additionally performs a global side effect, the documentation does not fully match the implementation intent.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
89% confidence
Finding

The code explicitly supports only English, Spanish, Japanese, and Chinese character heuristics, and falls back to 'en' for all other inputs. This imposes an implicit locale policy in behavior without offering users a language choice or documenting that the estimator is optimized for a narrow set of languages.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
78% confidence
Finding

The doc/comment for before_prompt_build emphasizes reading prompt content for estimation, which implies transient analysis. The implementation instead persists the prompt body in an in-memory session map for later use by commands, adding stateful retention not reflected by the comment. While not a severe contradiction, it understates what the code actually does with prompt content.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
98% confidence
Finding

The comment on L0698 states the free counter resets at midnight UTC, but the implementation sets freeReset = now + 86400, which is simply 24 hours after the current check time rather than the next UTC midnight. This is an active contradiction between the inline intent documentation and the actual quota-reset behavior.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
76% confidence
Finding

The comments claim the local engine preview is always free and that API billing happens only on the "Run Estimate" click, but the component also accepts externally supplied prompt content via postMessage and later dispatches prompt content outward to parent windows/events. While estimate() itself is local, the surrounding documented intent suggests a more isolated preview flow than the code actually participates in.

Content

No source excerpt is available for this finding.