Back to skill

Security audit

Smart Accountant with GST Understanding

Security checks for vulnerabilities and agentic risk

Overview

This accounting skill openly manages a local ledger, but its posting and correction scripts can make financial changes without reliably enforcing the stated approval and audit controls.

Install only in a test or non-production accounting workspace unless you add real approval enforcement, dry-run/confirmation gates, atomic rectification, reversal idempotency, date-aware period controls, and read-only preview behavior. Treat the included scripts as capable of changing financial records in the local SQLite database.

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

T09 · Insecure Skill Coding Practices

Error
Location
tool/scripts/post-voucher.js:14
Finding
Maker-checker approval can be self-asserted or bypassed<![CDATA[ ## Vulnerability Details **File Location**: `tool/scripts/post-voucher.js:14-18`; related bypasses at `tool/scripts/rectify-entry.js:19-21` and `tool/scripts/reverse-voucher.js:14-21` **Vulnerability Type**: Client-controlled authorization state **Risk Level**: High ### Vulnerable Code ```javascript if (payload.voucher.status !== 'CONFIRMED' && payload.voucher.status !== 'POSTED') { console.error("Maker-Checker Rule Enforced: Voucher must be CONFIRMED before posting."); process.exit(1); } ``` The rectification command additionally forces the trusted state: ```javascript const payload = JSON.parse(fs.readFileSync(newPayloadPath, 'utf8')); payload.voucher.status = 'POSTED'; // Force post for rectification try { const id = engine.postVoucher(payload.voucher, payload.lines); ``` The reversal command also constructs a posted voucher without independent approval: ```javascript const reversedVoucher = { ...voucher, id: undefined, // let it auto-increment voucher_no: null, // generate new status: 'POSTED', narration: `Reversal of ${voucher.voucher_no}: ${voucher.narration || ''}` }; ``` ### Technical Analysis The posting command treats the `status` property of caller-supplied JSON as proof that a checker approved the voucher. A caller can set the property to either `CONFIRMED` or `POSTED`; there is no authenticated approval record, approver identity, role separation, signature, or binding between an approval and the exact contents of the voucher. The rectification and reversal paths bypass even this superficial check by directly assigning `POSTED`. This contradicts the documented maker-checker workflow, under which the agent may prepare a voucher but must not submit it until the user explicitly approves it. ### Attack Path 1. A local caller or accounting agent creates a voucher JSON file. 2. The caller sets `voucher.status` to `POSTED` or `CONFIRMED`. 3. The caller runs `node scripts/post-voucher.js payload.json`. 4. ...[truncated 751 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Never accept `CONFIRMED` or `POSTED` as trusted state from an input payload. - Store approvals in a separate database table containing: - Voucher or draft identifier - Cryptographic hash of all approved voucher fields and lines - Authenticated approver identity - Approval timestamp - Maker and checker roles - Approval status and revocation state - Require the posting transaction to retrieve and consume a valid approval associated with the exact voucher hash. - Enforce that maker and checker identities are different. - Make `POSTED` an internal state transition that only the posting engine can assign. - Apply the same approval enforcement to posting, reversal, and rectification. - Record authorization failures and successful approvals in an append-only audit log. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
tool/scripts/rectify-entry.js:14
Finding
Rectification is non-atomic and can leave a committed reversal without a replacement<![CDATA[ ## Vulnerability Details **File Location**: `tool/scripts/rectify-entry.js:14-27` **Vulnerability Type**: Non-atomic financial transaction workflow **Risk Level**: High ### Vulnerable Code ```javascript // 1. Reverse the original const revId = reverseVoucher(voucherNo); console.log(`Reversed original voucher. Reversal ID: ${revId}`); // 2. Post the new entry const payload = JSON.parse(fs.readFileSync(newPayloadPath, 'utf8')); payload.voucher.status = 'POSTED'; // Force post for rectification try { const id = engine.postVoucher(payload.voucher, payload.lines); console.log(`Successfully posted corrected voucher ID: ${id}`); } catch (e) { console.error("Correction failed:", e.message); process.exit(1); } ``` ### Technical Analysis Rectification is composed of two independent database transactions. The original voucher is reversed and committed before the replacement file is read, parsed, or validated. If file access, JSON parsing, validation, a uniqueness constraint, a foreign-key constraint, or replacement posting fails, the committed reversal is not rolled back. The JSON parsing operation is also outside the `try` block. A malformed or unavailable payload therefore terminates the process after the reversal has already modified the books. ### Attack Path 1. Select an existing voucher that can be reversed. 2. Supply a missing, malformed, unbalanced, duplicate, or otherwise invalid replacement payload. 3. Run `node scripts/rectify-entry.js <voucher_no> <invalid_payload.json>`. 4. `reverseVoucher()` posts and commits the reversal. 5. Reading, parsing, validating, or posting the replacement fails. 6. The command exits without restoring the original accounting position. 7. The database permanently contains the reversal but not the intended corrected voucher. ### Impact Assessment The flaw can cause partial accounting updates, incorrect balances, incomplete rectifications, and reconciliation failures. An actor with permission to run ...[truncated 236 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Read and parse the replacement payload before making any database change. - Fully validate the replacement, referenced ledgers, dates, amounts, GST treatment, period status, and authorization before reversal. - Execute the reversal, source-voucher state transition, replacement posting, and audit records inside one outer database transaction. - Roll back the entire rectification if any step fails. - Add an immutable relationship among the original voucher, reversal voucher, and corrected voucher. - Assign a rectification request ID and enforce uniqueness for retry safety. - Put file reading, JSON parsing, validation, and transaction execution under structured error handling. - Add tests that deliberately fail every stage and verify that no partial database state remains. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
tool/scripts/reverse-voucher.js:4
Finding
Voucher reversal permits repeated, chained, and state-invalid reversals<![CDATA[ ## Vulnerability Details **File Location**: `tool/scripts/reverse-voucher.js:4-24` **Vulnerability Type**: Missing state validation and reversal idempotency **Risk Level**: High ### Vulnerable Code ```javascript function reverseVoucher(voucherNo) { const voucher = db.prepare("SELECT * FROM vouchers WHERE voucher_no = ?").get(voucherNo); if (!voucher) throw new Error("Voucher not found"); const lines = db.prepare("SELECT * FROM lines WHERE voucher_id = ?").all(voucher.id); const reversedLines = lines.map(line => ({ ledger_id: line.ledger_id, debit: line.credit, // swap credit: line.debit })); const reversedVoucher = { ...voucher, id: undefined, // let it auto-increment voucher_no: null, // generate new status: 'POSTED', narration: `Reversal of ${voucher.voucher_no}: ${voucher.narration || ''}` }; return engine.postVoucher(reversedVoucher, reversedLines); } ``` ### Technical Analysis The reversal function checks only whether a voucher exists. It does not require the source voucher to be `POSTED`, reject a source already marked `REVERSED`, prevent reversal of another reversal, or atomically mark and link the original voucher. No `reversal_of` field or unique database constraint exists. The generated reversal also inherits fields such as the original request ID when present, producing inconsistent behavior depending on whether the source voucher had an idempotency key. Where no request ID exists, the same source can be reversed repeatedly. ### Attack Path 1. Identify a voucher without a request ID. 2. Run `node scripts/reverse-voucher.js <voucher_no>`. 3. A posted offsetting voucher is created. 4. Run the same command again with the same source voucher. 5. Another posted offsetting voucher is created because the original was not marked as reversed. 6. Repeat the operation or target one of the generated reversal vouchers to create chained rever ...[truncated 419 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Permit reversal only when the source voucher has the exact state `POSTED`. - Add a non-nullable or controlled `reversal_of` foreign key to reversal vouchers. - Add a unique constraint on `reversal_of` so a source can have at most one effective reversal. - Atomically create the reversal and transition the source voucher from `POSTED` to `REVERSED`. - Reject reversal of drafts, previews, confirmed-but-unposted vouchers, previously reversed vouchers, and reversal vouchers unless an explicitly designed workflow allows it. - Generate a unique idempotency key for each authorized reversal request. - Store the source and reversal relationship in the audit log. - Require checker approval for the reversal and bind that approval to the source voucher and reversal date. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
tool/lib/validators.js:31
Finding
Closed-period validation ignores the voucher date<![CDATA[ ## Vulnerability Details **File Location**: `tool/lib/validators.js:31-36`; related period storage at `tool/scripts/close-period.js:3-10` **Vulnerability Type**: Ineffective accounting period access control **Risk Level**: High ### Vulnerable Code ```javascript function validatePeriodOpen(date) { const periodStatus = db.prepare("SELECT value FROM config WHERE key = 'period_status'").get(); if (periodStatus && periodStatus.value === 'CLOSED') { throw new Error("Cannot post into a closed period."); } } ``` The closing command records only a global flag: ```javascript function closePeriod() { console.log("Starting period close..."); // Simple mock logic for setting config flag const stmt = db.prepare("INSERT OR REPLACE INTO config (key, value) VALUES ('period_status', 'CLOSED')"); stmt.run(); console.log("Period locked. Status set to CLOSED."); } ``` ### Technical Analysis Although `validatePeriodOpen` receives a voucher date, it never uses that parameter. Period state is represented by one global configuration value with no start date, end date, period identifier, close timestamp, or reopening history. Consequently, the implementation cannot distinguish a historically closed month from an open current month. If the global status is open or absent, a backdated voucher for any historical date passes this control. Conversely, setting the flag to closed blocks every date rather than only the intended period. ### Attack Path 1. Close an accounting period using the global flag. 2. Later clear, replace, or reopen the global status to allow current-period posting. 3. Prepare a voucher whose `date` falls within the previously closed period. 4. Submit it through preview or posting. 5. The validator reads only the current global status and does not compare the voucher date with closed-period boundaries. 6. The backdated voucher is accepted. ### Impact Assessment A caller can post into historically closed ...[truncated 286 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Create a dedicated periods table with period ID, start date, end date, status, closed timestamp, and authorized closer. - Strictly parse voucher dates in a documented format such as ISO `YYYY-MM-DD`. - Query the period containing the supplied voucher date and require that specific period to be open. - Reject invalid, ambiguous, out-of-range, or timezone-dependent dates. - Record controlled reopening as a separate audited action rather than overwriting a global flag. - Require authorized approval for close and reopen operations. - Add database triggers or a centralized posting API so alternative mutation paths cannot bypass period enforcement. - Test posting at period boundaries and into previously closed historical periods. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
tool/lib/validators.js:3
Finding
Incomplete numeric and journal-line validation permits semantically invalid vouchers<![CDATA[ ## Vulnerability Details **File Location**: `tool/lib/validators.js:3-28` **Vulnerability Type**: Improper input validation **Risk Level**: Medium ### Vulnerable Code ```javascript function validateCompleteness(voucherData, linesData) { if (!voucherData.date || !voucherData.amount || !voucherData.type) { throw new Error("Missing required voucher fields: date, amount, type"); } if (!linesData || linesData.length < 2) { throw new Error("A voucher must have at least two lines."); } for (let line of linesData) { if (!line.ledger_id) { throw new Error("All lines must specify a ledger_id"); } } } function validateDoubleEntry(linesData) { let sumDebit = 0; let sumCredit = 0; for (let line of linesData) { sumDebit += (line.debit || 0); sumCredit += (line.credit || 0); } // Using a small epsilon to avoid float math issues if (Math.abs(sumDebit - sumCredit) > 0.001) { throw new Error(`Unbalanced entry. Total Debits (${sumDebit}) != Total Credits (${sumCredit})`); } } ``` ### Technical Analysis The validators check field presence and mathematical equality but do not enforce the complete accounting data model. In particular, they do not: - Require finite numeric values for the header amount, debit, and credit. - Validate the date syntax or calendar validity. - Require total debits and credits to be greater than zero. - Reconcile the header amount with journal-line totals. - Prohibit both debit and credit from being populated on the same line. - Require ledger IDs to be valid integers before posting. - Enforce currency precision using fixed-point arithmetic. For example, a positive header amount and two lines with zero debit and zero credit pass the completeness and double-entry checks. Balanced line totals unrelated to the header amount also pass. ### Attack Path 1. Create a voucher with a valid type, nonzero header amount, and date ...[truncated 755 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Define and enforce a strict payload schema before accessing voucher properties. - Require all monetary values to be finite, numeric, nonnegative, and within configured limits. - Use integer minor units or an exact decimal library instead of binary floating-point values. - Require every line to have exactly one positive side: debit or credit, but not both. - Require total debits and credits to be equal and greater than zero. - Define how the voucher header amount relates to lines and enforce that invariant. - Validate ledger IDs as positive integers and verify that every referenced ledger is active and postable. - Validate voucher type, status, date, tax rate, narration length, and source-document fields. - Add database constraints where possible so invalid records cannot be introduced through another code path. - Add tests for zero entries, non-finite values, malformed dates, excessive precision, mixed debit/credit lines, and header-line mismatches. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
tool/lib/gst-engine.js:3
Finding
Voucher preview unexpectedly writes GST ledgers to the database<![CDATA[ ## Vulnerability Details **File Location**: `tool/lib/gst-engine.js:3-13`; reachable from `tool/scripts/preview-voucher.js:24-36` **Vulnerability Type**: Hidden persistent mutation in a preview operation **Risk Level**: Medium ### Vulnerable Code ```javascript function getTaxLedgerId(name) { const l = db.prepare("SELECT id FROM ledgers WHERE name = ?").get(name); if (!l) { // Fallback for demo purposes if tax ledgers aren't seeded yet const insert = db.prepare(` INSERT INTO ledgers (name, type, normal_balance) VALUES (?, 'Liability', 'Credit') `).run(name); return insert.lastInsertRowid; } return l.id; } ``` The preview path reaches this mutation through GST calculation: ```javascript if (payload.voucher.tax_rate) { const isPurchase = ['PE', 'CP', 'BP'].includes(payload.voucher.type); const taxLines = gstEngine.determineGST( payload.voucher.amount, payload.voucher.tax_rate, partyLedger?.state_code || companyState, companyState, isPurchase ); payload.lines = [...payload.lines, ...taxLines]; } ``` ### Technical Analysis `preview-voucher.js` is presented as a validation and review stage, but GST calculation calls `getTaxLedgerId`, which inserts missing ledgers into persistent storage. This mutation occurs before the preview's main validation block. If later journal or GST validation fails, the inserted ledgers remain because their creation is not wrapped in a rollback transaction. The generated tax ledgers are also always classified as `Liability` with a `Credit` normal balance. That may be unsuitable for input-tax ledgers and demonstrates why chart-of-account creation should not be an automatic side effect of preview. ### Attack Path 1. Initialize a database without the expected GST tax ledgers. 2. Create a payload with a nonzero `tax_rate`. 3. Run `node scripts/preview-voucher.js payload.json`. 4. `deter ...[truncated 720 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Make preview operations strictly read-only at the database level. - Seed required GST ledgers during schema migration or an explicitly approved onboarding process. - If a required ledger is missing, return a proposed master-data change rather than inserting it. - Require separate authorization for chart-of-account creation. - Split pure GST calculation from persistent ledger lookup or creation. - If temporary simulation state is necessary, use an in-memory structure or a transaction that is always rolled back. - Validate all input before performing any mutation. - Define correct account types and normal balances separately for input and output tax ledgers. - Add regression tests verifying that successful and failed previews leave the database byte-for-byte or logically unchanged. ]]>
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
  • Rogue AgentSelf-Modification, Session Persistence
Findings (19)

Ae1

High
Category
analysis-evasion
Content
ized, the system uses the scripts in `tool/scripts/` (e.g., `post-voucher.js`, `generate-report.js`) as the primary interface for all ledger operations.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
Agent may not:
- self approve
- auto submit without confirmation

Required Flow
Confidence
75% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
The DELETE example shows direct record removal with no accompanying warning that the action is irreversible and affects persisted user data. In markdown guidance for a persistence skill, destructive data operations should include an explicit caution so users understand the impact before applying the pattern.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The example `node sqlite-util.js exec "update customers set balance=0"` demonstrates a destructive bulk update without any warning, guardrail, or confirmation step. In an agent-operated wrapper, such examples normalize unsafe behavior and can lead to accidental mass data corruption or loss when an agent follows the pattern literally.

Intent-Code Divergence

Medium
Confidence
98% confidence
Finding
The utility wrapper accepts arbitrary SQL from command-line arguments and executes it directly with `db.prepare(sql).all()` or `db.exec(sql)`, which defeats the document's own guidance to use parameterized queries. In an agent context, this is especially risky because upstream prompts, tool inputs, or untrusted task data may be transformed into SQL, enabling injection, unauthorized reads, schema modification, or destructive writes.

Intent-Code Divergence

Medium
Confidence
86% confidence
Finding
The file states that historical ledger data must be append-only and explicitly forbids UPDATE operations on ledger/balance-like data. However, later sections direct the posting workflow to 'update derived balances' and to maintain materialized summary tables during posting, which introduces mutation behavior that conflicts with the earlier absolute immutability language.

Missing User Warnings

Medium
Confidence
83% confidence
Finding
This markdown section documents `db-maintenance.js` commands such as `VACUUM`, `ANALYZE`, and `wal_checkpoint(FULL)` plus backup/optimize/checkpoint actions, but it does not warn that these operations modify the database file, can lock or pause access, and should be run with care. Under the markdown-specific SQP-2 criteria, behaviours affecting system integrity should include a clear user-facing warning.

Missing User Warnings

Medium
Confidence
87% confidence
Finding
The `migrate-schema.js` section declares the only allowed schema change path, but it does not warn users that migrations can irreversibly alter data structures, break compatibility, or require verified backups and rollback plans. Because this markdown describes behaviour that can affect user data and system integrity, a clear warning is expected.

Session Persistence

Medium
Category
Rogue Agent
Content
reverse_charge_flag
)

Do NOT use simplistic rule:

if gstin_matches...
Confidence
75% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
→ Confirmed
→ Posted

Do not skip confirmation.

---
Confidence
85% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
Prevent if possible.
Correct if necessary.

Never auto-post without asking:

Are you sure of this entry?
Confidence
75% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Missing User Warnings

Medium
Confidence
84% confidence
Finding
This function inserts voucher records, line items, and audit-log entries, which are persistent data writes with business impact. The file contains no confirmation prompt, user-facing logging, or explanatory docstring/comment warning that calling this function will create posted accounting records and audit entries.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The script performs a destructive accounting workflow by automatically reversing an existing voucher and then force-setting the replacement voucher status to POSTED without any confirmation, dry-run, or independent validation. In a financial posting context, this increases the risk of accidental or unauthorized ledger changes, especially if the input file is incorrect, malformed, or supplied by an untrusted operator.

Vague Triggers

Medium
Confidence
96% confidence
Finding
The colloquial trigger list is intentionally broad and includes vague phrases such as 'adjust this', 'pass entry', and 'make receipt' that can map to multiple accounting actions. In an automation context, this can cause unintended invocation or transaction misclassification, potentially creating incorrect financial entries or prompting the wrong voucher workflow.

Natural-Language Policy Violations

Medium
Confidence
87% confidence
Finding
The file includes a section labeled "Indian + Global Usage" with locale-specific Hinglish phrases such as "journal karo," "contra maaro," and "bill punch karo" without indicating that this language convention is optional or region-specific. This can violate language/locale policy because the skill embeds a specific linguistic mode without documented user choice or justification.

Intent-Code Divergence

Low
Confidence
72% confidence
Finding
The preview script is documented as a dry-run with 'No writes,' yet the overall posting flow and audit rules describe mandatory audit-related actions around validation/preview/commit. That creates intent ambiguity about whether preview is strictly read-only or whether audit/enforcement activity may write records during preview.

Natural-Language Policy Violations

Low
Confidence
87% confidence
Finding
The example phrase "Bought stock on udhar" uses a Hindi/Urdu term, which bakes a specific locale/language assumption into the skill guidance. The file does not offer users a language choice or explain that the skill is intended for a specific regional context, so this can conflict with language/locale policy requirements.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"license": "ISC",
  "description": "",
  "dependencies": {
    "better-sqlite3": "^11.10.0"
  }
}
Confidence
97% confidence
Finding
The dependency version is not pinned to an exact release and uses a caret range (^11.10.0), which permits automatic installation of newer minor and patch versions. This can introduce supply-chain risk and reduce build reproducibility if a compromised or breaking upstream release is published and later resolved during install.

Missing User Warnings

Low
Confidence
84% confidence
Finding
This code reads posted voucher details and outputs dates, voucher numbers, debit/credit amounts, and narration directly to stdout. While the script has usage/error messages, it provides no disclosure that running it will reveal potentially sensitive accounting data, and there is no surrounding comment or docstring warning about that behavior.

Static analysis

No suspicious patterns detected.