Back to skill

Security audit

Qb Cli

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent QuickBooks CLI, but it needs Review because it can change live financial records and has under-disclosed OAuth and shell-handling risks.

Review carefully before installing. Use a QuickBooks sandbox first, pin or inspect the GitHub source before running the install, replace the hardcoded ngrok OAuth redirect with a user-controlled registered redirect, protect .env and token files, and require explicit human approval before send, delete, import, batch, or production operations.

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

T09 · Insecure Skill Coding Practices

Error
Location
src/qb/auth/oauth.py:15
Finding

OAuth authorization artifacts are routed through a hardcoded third-party ngrok endpoint

Content
View full analysis
tuple[str, str]: """Generate authorization URL and state token. Returns: Tuple of (authorization_url, state_token) """ state = secrets.token_hex(16) params = { "client_id": client_id, "redirect_uri": REDIRECT_URI, "response_type": "code", "scope": SCOPES, "state": state, } url = f"{AUTHORIZATION_URL}?{urlencode(params)}" return url, state ``` It is also used during token exchange: ```python response = httpx.post( TOKEN_URL, headers={ "Authorization": _basic_auth_header(client_id, client_secret), "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json", }, data={ "grant_type": "authorization_code", "code": auth_code, "redirect_uri": REDIRECT_URI, }, timeout=30.0, ) ``` ### Technical Analysis The OAuth redirect URI determines where Intuit sends the authorization response. That response can contain a short-lived authorization code, the QuickBooks realm identifier, the OAuth state value, and error information. The implementation routes this response through a fixed ngrok hostname that is not an Intuit domain and is not shown as infrastructure controlled by t ...[truncated 2472 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
src/qb/auth/oauth.py:78
Finding

Headless OAuth callback accepts authorization data without validating state or callback origin

Content
View full analysis
dict: """Parse a callback URL pasted by the user (headless/SSH flow). Returns: Dict with 'code' and 'realm_id' keys. """ parsed = urlparse(callback_url) params = parse_qs(parsed.query) code = params.get("code", [None])[0] realm_id = params.get("realmId", [None])[0] error = params.get("error", [None])[0] if error: raise OAuthError(f"Authorization denied: {error}") if not code: raise OAuthError( "No authorization code found in URL. " "Make sure you copied the full redirect URL." ) return {"code": code, "realm_id": realm_id} ``` The caller generates a new state but does not validate it in the pasted-callback branch: ```python auth_url, state = generate_auth_url(client_id) if callback_url: # Headless mode: user already has the redirect URL try: result = parse_callback_url(callback_url) except OAuthError as e: handle_error(ExitCode.AUTH_ERROR, str(e)) ``` ### Technical Analysis OAuth `state` binds an incoming authorization response to the login transaction initiated by the client. The interactive `wait_for_callback()` path compares the received state with the expected value, but the headless `--callback-url` path does not. `parse_callback_url()` also accepts any URL with a `code` query parameter. It does not validate: - The `state` parameter. - The URL scheme. - The callback hostname. - The callback port. - The callback path. - Whether `realmId` is present and valid. Furthermore, when `--callback-url` is used, `login()` generates a fresh state value that has no relationship to the prior `--print-url` invocation. The state from the printed authorization ...[truncated 1861 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
run.sh:18
Finding

Unescaped CLI arguments permit host shell-command injection in the Docker-group fallback

Content
View full analysis
/dev/null 2>&1; then exec docker compose -f "$SKILL_DIR/docker-compose.yml" run --rm qb "$@" else exec sg docker -c "docker compose -f \"$SKILL_DIR/docker-compose.yml\" run --rm qb $*" fi ``` ### Technical Analysis The direct Docker branch correctly preserves arguments with `"$@"`. The fallback branch instead interpolates `$*` into a string passed to `sg docker -c`. The `-c` argument is interpreted by a shell. Since the individual arguments are not shell-escaped, metacharacters contained in any CLI argument can alter the command structure. Relevant payload syntax includes semicolons, command substitutions, redirections, pipelines, logical operators, quotes, and embedded newlines. This is particularly dangerous for a CLI that accepts many free-form values, including customer names, memos, descriptions, queries, JSON documents, callback URLs, email addresses, and file paths. If any of these values are attacker-controlled and the fallback branch is reached, they can become host shell syntax rather than data passed to the container. ### Attack Path 1. Direct access to the Docker socket fails, causing `run.sh` to enter the `sg docker -c` fallback branch. 2. An attacker supplies or influences a CLI argument containing shell syntax, for example a customer name, memo, query, or JSON value with a command substitution or command separator. 3. `$*` concatenates all arguments into the shell command string without preserving safe argument boundaries. 4. `sg` invokes the command through a shell under the Docker group context. 5. The shell interprets the malicious metacharacters. 6. The injected command executes on the host rather than inside the intended `qb` container. ### Impact Assessment Exploitation permits ...[truncated 595 chars]
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Output HandlingUnvalidated Output Injection, Cross-Context Output, Unbounded Output
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (68)

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · README.md (reported line 19)May include surrounding context.

bash
git clone https://github.com/claw4business/quickbooks-online-cli.git ~/skills/qb-cli
cd ~/skills/qb-cli
cp .env.example .env
# Edit .env with your Client ID and Client Secret
docker compose build

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · README.md (reported line 20)May include surrounding context.

git clone https://github.com/claw4business/quickbooks-online-cli.git ~/skills/qb-cli cd ~/skills/qb-cli cp .env.example .env

Edit .env with your Client ID and Client Secret

docker compose build

text

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 50)May include surrounding context.

git clone https://github.com/claw4business/quickbooks-online-cli.git ~/skills/qb-cli cd ~/skills/qb-cli cp .env.example .env

Edit .env with your Client ID and Client Secret

docker compose build

text

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 51)May include surrounding context.

git clone https://github.com/claw4business/quickbooks-online-cli.git ~/skills/qb-cli cd ~/skills/qb-cli cp .env.example .env

Edit .env with your Client ID and Client Secret

docker compose build

text

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 28)May include surrounding context.

md
- linux
install:
  - run: "git clone https://github.com/claw4business/quickbooks-online-cli.git ~/skills/qb-cli"
  - run: "cp ~/skills/qb-cli/.env.example ~/skills/qb-cli/.env"
  - run: "docker compose -f ~/skills/qb-cli/docker-compose.yml build"
---

Credential Access

High
Category
Privilege Escalation
Confidence
87% confidence
Finding

The script sources .env as shell code, not as inert configuration data. If an attacker can modify that file, arbitrary commands in .env will execute with the user's privileges before the Docker command runs, turning credential loading into a code-execution vector.

Content

Scanner excerpt · run.sh (reported line 11)May include surrounding context.

sh
SKILL_DIR="$(cd "$(dirname "$0")" && pwd)"

# Load .env if it exists
if [ -f "$SKILL_DIR/.env" ]; then
    set -a
    source "$SKILL_DIR/.env"

Credential Access

High
Category
Privilege Escalation
Confidence
87% confidence
Finding

This finding points to the same unsafe pattern: treating .env as executable shell input. In a skill that manages QuickBooks/Intuit API credentials, compromising .env could lead both to code execution and theft or misuse of financial-account access tokens.

Content

Scanner excerpt · run.sh (reported line 12)May include surrounding context.

sh
SKILL_DIR="$(cd "$(dirname "$0")" && pwd)"

# Load .env if it exists
if [ -f "$SKILL_DIR/.env" ]; then
    set -a
    source "$SKILL_DIR/.env"
    set +a

Credential Access

High
Category
Privilege Escalation
Confidence
87% confidence
Finding

Although the static match is repetitive, it refers to the same security issue: arbitrary shell execution via sourced .env content. The financial-tool context increases risk because any stolen API secrets may enable access to invoices, payments, vendors, and reports.

Content

Scanner excerpt · run.sh (reported line 14)May include surrounding context.

sh
# Load .env if it exists
if [ -f "$SKILL_DIR/.env" ]; then
    set -a
    source "$SKILL_DIR/.env"
    set +a
fi

Intent-Code Divergence

High
Category
Not specified by scanner
Confidence
98% confidence
Finding

The function documentation says it starts a local server and waits for a callback, but the configured REDIRECT_URI is an external ngrok HTTPS endpoint rather than the local HTTP server bound to localhost:8844. This mismatch can break the OAuth flow and, more importantly, route authorization codes through an externally hosted callback path that is inconsistent with the security assumptions of the local state-checking logic.

Content

No source excerpt is available for this finding.

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · src/qb/auth/oauth.py (reported line 176)May include surrounding context.

python
return self._tokens

    def get_access_token(self, client_id: str, client_secret: str) -> str:
        """Get a valid access token, refreshing automatically if needed."""
        tokens = self.load_tokens()

        if time.time() >= tokens["expires_at"] - REFRESH_BUFFER_SECONDS:

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · src/qb/auth/tokens.py (reported line 68)May include surrounding context.

python
return self._tokens

    def get_access_token(self, client_id: str, client_secret: str) -> str:
        """Get a valid access token, refreshing automatically if needed."""
        tokens = self.load_tokens()

        if time.time() >= tokens["expires_at"] - REFRESH_BUFFER_SECONDS:

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · src/qb/commands/auth.py (reported line 143)May include surrounding context.

python
return self._tokens

    def get_access_token(self, client_id: str, client_secret: str) -> str:
        """Get a valid access token, refreshing automatically if needed."""
        tokens = self.load_tokens()

        if time.time() >= tokens["expires_at"] - REFRESH_BUFFER_SECONDS:

Unvalidated Output Injection

High
Category
Output Handling
Confidence
97% confidence
Finding

The code interpolates the user-controlled account_type directly into a SQL-like QuickBooks query string using single quotes, allowing crafted input to alter the WHERE clause. In this financial-management CLI context, query manipulation can expose broader accounting data than intended and may bypass the command's built-in filters, which is especially sensitive because the tool is designed for direct agent consumption.

Content

Scanner excerpt · src/qb/commands/account.py (reported line 39)May include surrounding context.

python
if account_type:
        clauses.append(f"AccountType = '{account_type}'")
    where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
    result = client.query(f"SELECT * FROM Account {where}", max_results=limit)
    accounts = result.get("QueryResponse", {}).get("Account", [])
    format_output(
        accounts,

Unvalidated Output Injection

High
Category
Output Handling
Confidence
95% confidence
Finding

Model output is used without validation or sanitization. Unvalidated output injected into downstream contexts (SQL, shell, HTML) enables injection attacks and arbitrary code execution.

Content

Scanner excerpt · src/qb/commands/customer.py (reported line 33)May include surrounding context.

python
fmt = output or _output()
    client = _client()
    where = "WHERE Active = true" if active_only else ""
    result = client.query(f"SELECT * FROM Customer {where}", max_results=limit)
    customers = result.get("QueryResponse", {}).get("Customer", [])
    format_output(
        customers,

Unvalidated Output Injection

High
Category
Output Handling
Confidence
95% confidence
Finding

Model output is used without validation or sanitization. Unvalidated output injected into downstream contexts (SQL, shell, HTML) enables injection attacks and arbitrary code execution.

Content

Scanner excerpt · src/qb/commands/customer.py (reported line 97)May include surrounding context.

python
if not results:
        try:
            active_clause = "WHERE Active = true" if not include_inactive else ""
            resp = client.query(f"SELECT * FROM Customer {active_clause}", max_results=500)
            for cust in resp.get("QueryResponse", {}).get("Customer", []):
                phone = (cust.get("PrimaryPhone") or {}).get("FreeFormNumber", "")
                if term.lower() in phone.lower() and cust["Id"] not in seen_ids:

Unvalidated Output Injection

High
Category
Output Handling
Confidence
95% confidence
Finding

User-controlled input from --type is interpolated directly into the SQL-like query string without escaping or validation. An attacker can supply crafted input containing quotes and additional predicates to alter the query semantics, potentially bypassing intended filters or retrieving unintended records from the QuickBooks API.

Content

Scanner excerpt · src/qb/commands/item.py (reported line 39)May include surrounding context.

python
if item_type:
        clauses.append(f"Type = '{item_type}'")
    where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
    result = client.query(f"SELECT * FROM Item {where}", max_results=limit)
    items = result.get("QueryResponse", {}).get("Item", [])
    format_output(
        items,

Unvalidated Output Injection

High
Category
Output Handling
Confidence
95% confidence
Finding

Model output is used without validation or sanitization. Unvalidated output injected into downstream contexts (SQL, shell, HTML) enables injection attacks and arbitrary code execution.

Content

Scanner excerpt · src/qb/commands/tax.py (reported line 31)May include surrounding context.

python
fmt = output or _output()
    client = _client()
    where = "WHERE Active = true" if active_only else ""
    result = client.query(f"SELECT * FROM TaxCode {where}", max_results=100)
    codes = result.get("QueryResponse", {}).get("TaxCode", [])
    format_output(
        codes,

Missing User Warnings

High
Category
Not specified by scanner
Confidence
98% confidence
Finding

The delete command fetches the current transfer and then immediately submits a destructive delete operation with no warning, confirmation, or safety interlock. Because this tool is designed for agent consumption and manages financial records, a wrong ID, malicious prompt injection, or automation error can irreversibly alter accounting data and audit history.

Content

No source excerpt is available for this finding.

Unvalidated Output Injection

High
Category
Output Handling
Confidence
95% confidence
Finding

Model output is used without validation or sanitization. Unvalidated output injected into downstream contexts (SQL, shell, HTML) enables injection attacks and arbitrary code execution.

Content

Scanner excerpt · src/qb/commands/vendor.py (reported line 33)May include surrounding context.

python
fmt = output or _output()
    client = _client()
    where = "WHERE Active = true" if active_only else ""
    result = client.query(f"SELECT * FROM Vendor {where}", max_results=limit)
    vendors = result.get("QueryResponse", {}).get("Vendor", [])
    format_output(
        vendors,

Unvalidated Output Injection

High
Category
Output Handling
Confidence
95% confidence
Finding

Model output is used without validation or sanitization. Unvalidated output injected into downstream contexts (SQL, shell, HTML) enables injection attacks and arbitrary code execution.

Content

Scanner excerpt · src/qb/commands/workflow.py (reported line 79)May include surrounding context.

python
# Check 2: Overdue AR
    try:
        resp = client.query(f"SELECT * FROM Invoice WHERE Balance > '0' AND DueDate < '{start_date}'", max_results=500)
        overdue_invoices = resp.get("QueryResponse", {}).get("Invoice", [])
        overdue_total = sum(float(inv.get("Balance", 0)) for inv in overdue_invoices)
        checks.append({

Unvalidated Output Injection

High
Category
Output Handling
Confidence
95% confidence
Finding

Model output is used without validation or sanitization. Unvalidated output injected into downstream contexts (SQL, shell, HTML) enables injection attacks and arbitrary code execution.

Content

Scanner excerpt · src/qb/commands/workflow.py (reported line 94)May include surrounding context.

python
# Check 3: Overdue AP
    try:
        resp = client.query(f"SELECT * FROM Bill WHERE Balance > '0' AND DueDate < '{start_date}'", max_results=500)
        overdue_bills = resp.get("QueryResponse", {}).get("Bill", [])
        overdue_bill_total = sum(float(b.get("Balance", 0)) for b in overdue_bills)
        checks.append({

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The README explicitly promotes direct use against QuickBooks accounts, including production via QB_ENVIRONMENT, and demonstrates state-changing operations like invoice creation without any warning about the risk of modifying live financial records. In an agent skill context, this increases the chance of accidental misuse, test commands being run in production, or unauthorized bookkeeping changes with real business impact.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
84% confidence
Finding

The README instructs users to copy .env.example to .env and place the QuickBooks Client ID and Client Secret there, but gives no warning that these are sensitive credentials requiring careful storage and exclusion from source control. In an AI-agent and Docker-based setup, this raises the risk of secrets being committed, exposed through logs, or mounted into containers without proper handling.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
96% confidence
Finding

The skill exposes high-risk capabilities including shell execution, network access, file read/write, and environment-variable use, but it does not declare any explicit tool scope such as allowed-tools or permissions. For an agent-consumable skill that can create, update, delete, and send financial records, this omission weakens policy enforcement and increases the chance of overbroad or unintended actions.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The bank import section states that unmatched transactions are created as Purchase or Deposit records, but the workflow does not present this as a prominent user-facing warning before the non-dry-run command. In a bookkeeping context, an agent could import a statement and unintentionally create financial transactions, causing accounting inaccuracies or duplicate records.

Content

No source excerpt is available for this finding.