Back to skill

Security audit

Bring Rezepte

Security checks for vulnerabilities and agentic risk

Overview

The skill fits its Bring shopping-list purpose, but it has a real account-token leak risk and weaker credential handling that should be reviewed before installation.

Review or fix the unrestricted --content-url behavior before installing, and only fetch official Bring template-content URLs. Provide Bring credentials through a protected environment or secret manager rather than --password, keep list modifications behind explicit confirmation, and install using the committed lockfile.

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

Error
Location
scripts/bring_list.js:373
Finding
Authenticated Bring Headers Can Be Exfiltrated to an Arbitrary Host<![CDATA[ ## Vulnerability Details **File Location**: `scripts/bring_list.js:46-54, 373-381` **Vulnerability Type**: Arbitrary destination request with sensitive authentication headers **Risk Level**: High ### Vulnerable Code ```js async function fetchContent(url, headers) { const resp = await fetch(url, { headers }); const text = await resp.text(); try { return JSON.parse(text); } catch (err) { throw new Error(`Non-JSON response (${resp.status}): ${text.slice(0, 200)}`); } } ``` ```js if (contentUrlRaw) { const urls = contentUrlRaw .split(",") .map((s) => s.trim()) .filter(Boolean); const allItems = []; for (const url of urls) { const content = await fetchContent(url, bring.headers); const items = extractItemsFromContent(content); allItems.push(...items); } ``` The sensitivity of `bring.headers` is confirmed by `references/bring-inspirations.md:12-18`: ```md 2. Set headers for subsequent calls: - `X-BRING-API-KEY`: `<public client key from bring-shopping npm package>` - `X-BRING-CLIENT`: `webApp` - `X-BRING-CLIENT-SOURCE`: `webApp` - `X-BRING-COUNTRY`: `DE` (use user locale country if known) - `X-BRING-USER-UUID`: `<uuid from login>` - `Authorization`: `Bearer <access_token>` ``` ### Technical Analysis The `--content-url` option accepts a caller-controlled, comma-separated collection of URLs. The code does not validate the URL scheme, hostname, port, or path before passing each URL to `fetchContent`. After the Skill logs into the Bring service, it passes the complete `bring.headers` object to `fetch`. According to the bundled API reference, that object contains an OAuth-style bearer access token and the user's Bring UUID. Consequently, a direct URL pointing to an attacker-controlled server receives headers intended only for the Bring API. The declared functionality only requires authenticated requests to Bring template endpoints such as: ```text https://api.getbring.com/rest/v2/bring ...[truncated 1942 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse every supplied URL using `new URL()` and reject malformed values. 2. Require the `https:` scheme. 3. Allow only the expected hostname: ```js parsed.hostname === "api.getbring.com" ``` 4. Require the expected path prefix: ```text /rest/v2/bringtemplates/content/ ``` 5. Reject embedded credentials, non-default ports, IP literals, fragments, and unexpected query parameters. 6. Disable redirects with `redirect: "error"` so an approved Bring URL cannot redirect the request to another origin. 7. Construct an explicit minimum header allowlist rather than passing the complete mutable `bring.headers` object. 8. Prefer accepting a Bring content UUID rather than a full URL, then construct the trusted URL internally. 9. Apply request timeouts and response-size limits to reduce denial-of-service exposure. 10. Add tests proving that external domains, HTTP URLs, alternate ports, loopback addresses, private addresses, and redirects are rejected. A safer design would resemble: ```js function buildContentUrl(contentUuid) { if (!/^[0-9a-f-]+$/i.test(contentUuid)) { throw new Error("Invalid Bring content UUID."); } return new URL( `/rest/v2/bringtemplates/content/${contentUuid}`, "https://api.getbring.com" ); } async function fetchBringContent(contentUuid, bringHeaders) { const url = buildContentUrl(contentUuid); const response = await fetch(url, { headers: { Authorization: bringHeaders.Authorization, "X-BRING-API-KEY": bringHeaders["X-BRING-API-KEY"], "X-BRING-CLIENT": bringHeaders["X-BRING-CLIENT"], "X-BRING-CLIENT-SOURCE": bringHeaders["X-BRING-CLIENT-SOURCE"], "X-BRING-COUNTRY": bringHeaders["X-BRING-COUNTRY"], "X-BRING-USER-UUID": bringHeaders["X-BRING-USER-UUID"], }, redirect: "error", }); if (!response.ok) { throw new Error(`Bring content request failed: ${response.status}`); } return response.json(); } ``` ]]>

T09 · Insecure Skill Coding Practices

Note
Location
scripts/bring_list.js:112
Finding
Bring Account Password Can Be Supplied Through Exposed Command-Line Arguments<![CDATA[ ## Vulnerability Details **File Location**: `scripts/bring_list.js:112-121`; duplicated in `scripts/bring_inspirations.js:64-73`; documented in `SKILL.md:127` **Vulnerability Type**: Sensitive credential exposure through process arguments and command history **Risk Level**: Low ### Vulnerable Code From `scripts/bring_list.js`: ```js const email = args.email || process.env.BRING_EMAIL; const password = args.password || process.env.BRING_PASSWORD; const country = args.country || process.env.BRING_COUNTRY || "DE"; if (!email || !password) { throw new Error("Missing BRING_EMAIL/BRING_PASSWORD (or --email/--password)."); } const Bring = loadBring(); const bring = new Bring({ mail: email, password }); ``` The same behavior appears in `scripts/bring_inspirations.js`: ```js const email = args.email || process.env.BRING_EMAIL; const password = args.password || process.env.BRING_PASSWORD; const country = args.country || process.env.BRING_COUNTRY || "DE"; if (!email || !password) { throw new Error("Missing BRING_EMAIL/BRING_PASSWORD (or --email/--password)."); } const Bring = loadBring(); const bring = new Bring({ mail: email, password }); ``` `SKILL.md` explicitly recommends this fallback: ```md If ENV is not set, pass `--email` and `--password` explicitly. ``` ### Technical Analysis Both scripts parse `--password` directly from `process.argv`. Secrets supplied through command-line arguments can be exposed through shell history, process inspection facilities, audit systems, orchestration metadata, diagnostic reports, and command logging. Exposure through process listings depends on operating-system permissions and runtime configuration, but shell history and automation logs remain common disclosure channels. Supporting an environment variable is preferable to command-line input, although a dedicated secret manager or protected input channel would provide stronger controls. Accepting a password is necessary for the documented Bring login flow, ...[truncated 1262 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove support for the `--password` command-line option from both scripts. 2. Remove the documentation that recommends passing passwords as command-line arguments. 3. Prefer a protected secret manager supplied by the execution platform. 4. If interactive use is required, read the password from a hidden terminal prompt without echoing it. 5. Continue supporting `BRING_PASSWORD` only where the runtime protects environment variables from unauthorized users and logs. 6. Ensure errors and debug output never serialize the password, full environment, Bring client object, or login request. 7. Document secure credential rotation procedures in case a password was previously supplied on the command line. 8. Advise users to remove affected entries from shell history and rotate credentials if exposure is suspected. For example, reject command-line password input and use a protected environment value: ```js if (args.password) { throw new Error( "--password is not supported because command-line arguments may be exposed. " + "Use BRING_PASSWORD or a protected secret provider." ); } const email = process.env.BRING_EMAIL; const password = process.env.BRING_PASSWORD; if (!email || !password) { throw new Error( "Missing credentials. Configure BRING_EMAIL and BRING_PASSWORD through a protected secret provider." ); } ``` ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (13)

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill uses sensitive capabilities including environment-provided credentials and network access, but does not declare an explicit tool scope such as allowed-tools or permissions. This creates an authorization gap where the runtime may permit broader tool use than intended, making it harder to enforce least privilege and increasing the risk of accidental or unauthorized external actions.

Vague Triggers

Medium
Confidence
92% confidence
Finding
The activation guidance is broad enough to trigger on general meal-planning and shopping-related requests, which can cause the agent to invoke a credentialed, networked skill in situations where the user did not clearly ask for Bring account actions. Because the skill can list shopping lists and add items after follow-up prompts, over-triggering increases the chance of unnecessary data access and unintended account modification.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The skill documentation instructs the agent to add items to a Bring shopping list but provides no requirement for explicit user confirmation before modifying persistent user data. In an agent setting, this can lead to unauthorized or accidental list changes, especially when recipe suggestions are generated or selected automatically from external content.

Context-Inappropriate Capability

Medium
Confidence
91% confidence
Finding
The script accepts Bring account credentials via --email and --password CLI flags, which commonly exposes secrets through shell history, process listings, job-control logs, and orchestration telemetry. Because this skill’s purpose is recipe inspiration and URL parsing, taking raw credentials on the command line is not necessary and increases the chance of accidental credential disclosure rather than serving a security need.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
This code logs into an external Bring service with user credentials and later sends recipe URLs to the service via API calls, but the file provides no comment, docstring, or user-facing notice explaining that credentials and supplied URLs will be transmitted over the network. For a code file, these network and credential-handling operations meet the safety-critical criteria and lack visible disclosure in the file.

Context-Inappropriate Capability

Medium
Confidence
98% confidence
Finding
The script loads executable code from BRING_NODE_API_PATH via require(envPath), allowing whoever controls the environment to substitute an arbitrary module. In an agent or automation context, environment variables are often configurable by deployment tooling or wrappers, so this becomes a code-execution hook unrelated to normal shopping-list functionality.

External Transmission

Medium
Category
Data Exfiltration
Content
// Create the list via the Bring API (POST with form-urlencoded)
    const userUuid = bring.uuid || bring.headers["X-BRING-USER-UUID"];
    const baseUrl = bring.url || "https://api.getbring.com/rest/v2/";
    const createUrl = `${baseUrl}bringusers/${userUuid}/lists`;

    const formBody = new URLSearchParams({
Confidence
60% 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
// Create the list via the Bring API (POST with form-urlencoded)
    const userUuid = bring.uuid || bring.headers["X-BRING-USER-UUID"];
    const baseUrl = bring.url || "https://api.getbring.com/rest/v2/";
    const createUrl = `${baseUrl}bringusers/${userUuid}/lists`;

    const formBody = new URLSearchParams({
Confidence
60% 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
// Create the list via the Bring API (POST with form-urlencoded)
    const userUuid = bring.uuid || bring.headers["X-BRING-USER-UUID"];
    const baseUrl = bring.url || "https://api.getbring.com/rest/v2/";
    const createUrl = `${baseUrl}bringusers/${userUuid}/lists`;

    const formBody = new URLSearchParams({
Confidence
60% 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
// Create the list via the Bring API (POST with form-urlencoded)
    const userUuid = bring.uuid || bring.headers["X-BRING-USER-UUID"];
    const baseUrl = bring.url || "https://api.getbring.com/rest/v2/";
    const createUrl = `${baseUrl}bringusers/${userUuid}/lists`;

    const formBody = new URLSearchParams({
Confidence
60% 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
// Create the list via the Bring API (POST with form-urlencoded)
    const userUuid = bring.uuid || bring.headers["X-BRING-USER-UUID"];
    const baseUrl = bring.url || "https://api.getbring.com/rest/v2/";
    const createUrl = `${baseUrl}bringusers/${userUuid}/lists`;

    const formBody = new URLSearchParams({
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
The instruction 'Keep the skill output in German for Germany by default' imposes a language/locale preference in natural language without stating that the user may choose another language. The policy allows locale constraints when justified or opt-in is offered, but neither is present here.

Unpinned Dependencies

Low
Category
Supply Chain
Content
{
  "type": "commonjs",
  "dependencies": {
    "bring-shopping": "^2.0.1"
  }
}
Confidence
92% confidence
Finding
The dependency version is specified with a caret range (^2.0.1), which allows newer compatible releases to be installed automatically. This creates supply-chain risk because a compromised or malicious upstream release could be pulled in without code changes, and this skill interacts with shopping-list/account functionality where a dependency compromise could affect user data or perform unauthorized actions.

Static analysis

Detected: suspicious.env_credential_access

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
scripts/bring_list.js:5