Back to skill

Security audit

India Tax Helper

Security checks for vulnerabilities and agentic risk

Overview

The skill does not show data theft or persistence, but its tax-rule packaging and calculators are inconsistent enough that users should review it before relying on it.

Install only if you are comfortable treating this as conceptual guidance and reviewing calculator results manually. Do not rely on its tax estimates until the missing FY rules/source manifest are included and the known calculation issues are fixed or clearly scoped as non-final estimates.

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

Warning
Location
scripts/capital_gains_estimator.py:42
Finding
Standalone Capital-Gains Calculator Ignores the Long-Term Capital-Gains Exemption<![CDATA[ ## Vulnerability Details **File Location**: `scripts/capital_gains_estimator.py`, lines 42-45 **Vulnerability Type**: Incorrect tax calculation caused by omitted exemption handling **Risk Level**: Medium ### Vulnerable Code ```python long_term = holding_days >= int(rule['long_term_days']) rate = float(rule['lt_rate'] if long_term else rule['st_rate']) taxable = max(0.0, gain) tax = round(taxable * rate, 2) ``` The incorrect behavior is explicitly preserved by the test at `scripts/test_suite.py`, lines 106-116: ```python def test_capital_gains_lt(): r = run('capital_gains_estimator.py', { 'fy': 'FY-2026-27', 'asset_type': 'equity_stt_paid', 'gain': 50000, 'holding_days': 400 }) assert r['result']['classification'] == 'long_term' assert_approx(r['result']['estimated_tax'], 6250, msg="LTCG equity") # 12.5% of 50K (above 1.25L exemption, but 50K < 1.25L so... wait) # Actually 50K gain is below 1.25L exemption, so tax should be 0! # But the script doesn't apply the exemption, it just applies the rate # This is a known simplification; the exemption is applied in full_tax_estimator print("PASS: capital_gains_lt (rate check)") ``` ### Technical Analysis The calculator determines whether a gain is long-term and selects the corresponding rate, but applies that rate to the entire positive gain. It does not read or apply the rule's `lt_exemption_limit`. Consequently, an amount that falls entirely within the verified exemption can still be reported as taxable. The test suite confirms that this is known behavior and treats the incorrect result as passing. The script is described as a capital-gains tax estimator rather than only a rate lookup. Its output fields—`taxable_gain_used` and `estimated_tax`—can therefore reasonably be interpreted as an estimated liability. ### Attack Path 1. A user supplies an equity capital gain below the configured long-term exemption. 2. The user supplies a holding period that cl ...[truncated 865 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Read the verified `lt_exemption_limit` from the selected capital-gains rule. 2. Apply the exemption only to eligible long-term gains: ```python if long_term: exemption = float(rule.get('lt_exemption_limit', 0)) taxable = max(0.0, gain - exemption) else: taxable = max(0.0, gain) ``` 3. If the script is intended only to demonstrate an applicable rate, rename the script and output fields accordingly and do not label the result as estimated tax liability. 4. Replace the test expectation of `6250` with `0` for a ₹50,000 gain under a ₹125,000 exemption. 5. Add tests for gains below, equal to, and above the exemption, as well as losses and unsupported asset types. 6. Continue to fail closed when the selected rule does not contain sufficient information to calculate the exemption safely. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/full_tax_estimator.py:103
Finding
Full Tax Estimator Incorrectly Taxes Ordinary Income Components in Isolation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/full_tax_estimator.py`, lines 103-107, 130-136, and 157 **Vulnerability Type**: Incorrect aggregate slab-tax computation **Risk Level**: High ### Vulnerable Code Salary income is taxed independently: ```python # Salary tax taxable_salary = max(0.0, gross_salary - deductions - std_ded) salary_base_tax = compute_tax_from_slabs(taxable_salary, slabs) ``` Other ordinary income and FD interest are then independently passed through the same slab schedule: ```python # Other income tax (at slab rates for old regime; slab rates for new regime too) other_tax = 0.0 if other_income > 0: other_tax = compute_tax_from_slabs(other_income, slabs) # FD interest: taxed at slab rate (no special rate) fd_tax = 0.0 if fd_interest > 0: fd_tax = compute_tax_from_slabs(fd_interest, slabs) ``` The independently computed values are added together: ```python total_tax = round(total_salary_tax + other_tax + fd_tax + cg_tax, 2) ``` ### Technical Analysis Progressive slab taxation generally depends on aggregate slab-taxable income. The implementation instead restarts the slab schedule separately for salary, other income, and FD interest. This can incorrectly place each additional component into lower or zero-rate bands rather than taxing it at the user's applicable marginal rate. Rebate, surcharge, and cess are also not consistently calculated from the final combined tax base: - The salary slab tax is computed before including ordinary other income. - Rebate is described and implemented as applying to salary tax only. - Surcharge is derived from combined income but applied only to the post-rebate salary tax. - Cess is initially calculated only from that salary-related amount. - Independently calculated other-income, FD-interest, and capital-gains tax amounts are added afterward without a corresponding aggregate cess calculation. The resulting value is therefore not a reliable end-to-end tax liability despite b ...[truncated 1048 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Aggregate all income taxed at slab rates before calculating progressive tax: ```python ordinary_taxable_income = max( 0.0, gross_salary + other_income + fd_interest - deductions - std_ded ) ordinary_base_tax = compute_tax_from_slabs(ordinary_taxable_income, slabs) ``` 2. Keep income subject to special rates separate, but integrate it according to the verified rules governing: - Basic exemption adjustment - Rebate eligibility - Surcharge - Marginal relief - Cess 3. Apply rebate, surcharge, marginal relief, and cess to the legally appropriate aggregate tax and income bases rather than only to the salary component. 4. Do not emit a total liability when required interaction rules are absent from the verified rules file. Return `blocked_unverified_rules` instead. 5. Add tests covering: - Salary near every slab boundary plus FD interest - Salary plus ordinary other income - Ordinary income combined with special-rate capital gains - Rebate thresholds - Surcharge thresholds and marginal relief - Cess over all taxable components 6. Have the revised calculation model independently reviewed against official computation examples before presenting results as end-to-end estimates. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/regime_comparator.py:39
Finding
Regime Comparator Uses Hard-Coded FY 2026-27 Rules While Reporting the Requested FY<![CDATA[ ## Vulnerability Details **File Location**: `scripts/regime_comparator.py`, lines 39-41 and 93-96 **Vulnerability Type**: Tax-year confusion caused by hard-coded rule selection **Risk Level**: High ### Vulnerable Code The computation function always selects FY 2026-27: ```python def compute_regime_tax(taxable_income: float, regime: str, age: int, rules: dict): fy_rules = rules.get('salary_regimes', {}).get('FY-2026-27', {}).get(regime) if not fy_rules: return None ``` The caller separately accepts and later reports an arbitrary FY: ```python fy = data.get('fy', 'FY-2026-27') age = int(data.get('age', 30)) gross_salary = float(data.get('gross_salary', 0)) ``` The selected `fy` is not passed to `compute_regime_tax`, but is included in the final result: ```python { 'fy': fy, 'age': age, 'gross_salary': gross_salary, ``` ### Technical Analysis The script creates a discrepancy between calculation provenance and output labeling. Although input accepts an FY and output echoes it, both regime calculations always load the `FY-2026-27` rule entry. If a rules file contains several years, a request for another year can produce a valid-looking result calculated with the wrong slabs, standard deductions, rebate rules, and potentially surcharge rules. The script does not fail closed in this case because the hard-coded FY entry may exist. This violates the Skill's stated anti-staleness and FY-verification guarantees. ### Attack Path 1. A rules file contains FY 2026-27 and one or more other FY entries. 2. A caller requests a regime comparison for an FY other than FY 2026-27. 3. The script stores the requested FY in `fy`. 4. `compute_regime_tax` ignores that value and selects FY 2026-27. 5. The old/new comparison and winner are calculated under FY 2026-27 rules. 6. The output labels the result with the caller's requested FY. 7. A user may select a tax regime based on a falsely labeled comparison. No operating-system privileges a ...[truncated 462 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Add the FY as an explicit argument to `compute_regime_tax`: ```python def compute_regime_tax( taxable_income: float, regime: str, age: int, fy: str, rules: dict ): fy_rules = rules.get('salary_regimes', {}).get(fy, {}).get(regime) if not fy_rules: return None ``` 2. Pass the parsed FY into both calls: ```python new_result = compute_regime_tax(taxable_new, 'new', age, fy, rules) old_result = compute_regime_tax(taxable_old, 'old', age, fy, rules) ``` 3. Fail closed with a specific error when the requested FY or either regime entry is unavailable. 4. Source standard deductions from the selected FY's regime rules rather than global fallback values where possible. 5. Add multi-year tests with deliberately different slabs so that hard-coded or mismatched FY selection cannot pass unnoticed. 6. Include the actual ruleset identifier or digest in output to make calculation provenance auditable. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
scripts/common.py:21
Finding
Caller-Controlled Rules Files Are Trusted Based Only on a Boolean Flag<![CDATA[ ## Vulnerability Details **File Location**: `scripts/common.py`, lines 21-29 **Vulnerability Type**: Insufficient validation of trusted financial rules **Risk Level**: Low ### Vulnerable Code ```python def load_rules(path: str | None): if not path: raise RulesError('No verified rules file provided') p = Path(path) if not p.exists(): raise RulesError(f'Rules file not found: {path}') data = load_json(str(p)) if not data.get('verified'): raise RulesError('Rules file is not marked verified=true') return data ``` ### Technical Analysis All calculators accept a caller-supplied `--rules` path. `load_rules` treats any existing JSON document containing a truthy `verified` field as verified. The function does not: - Restrict rules to an approved project directory - Bind a rules file to the source manifest - Verify a cryptographic digest or signature - Validate a strict schema - Confirm that the FY and source metadata are internally consistent - Distinguish bundled rules from arbitrary externally supplied files The boolean is therefore self-asserted by the same data it is intended to authenticate. This does not establish provenance or integrity. Exploitation requires the ability to provide a local rules file and influence script invocation. It does not independently create arbitrary file-write or command-execution capability. ### Attack Path 1. An attacker or untrusted integration creates a JSON rules file containing fabricated rates and `"verified": true`. 2. The calculator is invoked with `--rules` pointing to that file. 3. `load_rules` confirms only that the file exists and the flag is truthy. 4. The fabricated rules are accepted as verified. 5. The calculator returns tax results derived from attacker-controlled rates while retaining the appearance of verified computation. ### Impact Assessment The flaw can compromise the integrity of all calculator outputs when invocation parameters are attacker-infl ...[truncated 288 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Resolve rule files from a fixed, bundled directory based on a validated FY identifier rather than accepting arbitrary paths during normal operation. 2. If external rule files must be supported, treat them as unverified unless their integrity and provenance are independently established. 3. Define and enforce a strict JSON schema covering: - Supported FY format - Required slab structure - Numeric range constraints - Rebate, surcharge, cess, and exemption fields - Source metadata 4. Bind each approved rules file to a trusted source manifest using a cryptographic digest or signature stored outside the rules file. 5. Resolve paths and reject files outside the approved rules directory: ```python approved_root = (PROJECT_ROOT / 'references').resolve() candidate = Path(path).resolve() if approved_root not in candidate.parents: raise RulesError('Rules file is outside the approved rules directory') ``` 6. Do not use a self-declared `verified` boolean as the sole trust decision. 7. Include a verified ruleset digest in calculator output so downstream users can confirm which rules produced the result. ]]>
Vulnerability Patterns
  • System Prompt LeakageDirect Leakage, Indirect Extraction, Tool-Based Exfiltration
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (10)

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The declared description presents an end-user tax advisory skill covering multiple Indian tax topics with conservative guidance and deterministic calculators. The supplied code does not implement those functions. It only loads a local JSON manifest of sources and selects candidate references for a topic using keyword heuristics. While this could be a supporting internal component for a tax helper, the code chunk itself materially differs from the declared primary purpose and lacks the described user-facing tax analysis capabilities. There is no evidence of prompt injection handling concerns affecting this assessment.

Direct Prompt Extraction

High
Category
System Prompt Leakage
Content
return
    if gain is None:
        dump(blocked('Missing gain/loss amount', ['gain']))
        return

    rule = rules.get('capital_gains', {}).get(fy, {}).get(asset_type)
    if not rule:
Confidence
85% confidence
Finding
Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Lp3

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding
Without declared permissions the skill's intent is opaque and cannot be validated.

Vague Triggers

Medium
Confidence
93% confidence
Finding
The activation rule 'Use for users asking only about salary, Form 16, employer declaration, and ITR basics' is broad and underspecified, especially in a tax skill where small scope changes can materially alter legal/tax guidance. A vague trigger can cause the agent to activate for adjacent topics such as capital gains, multiple house properties, non-resident status, or current-FY rule questions, leading to overconfident or inapplicable advice instead of failing closed.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The module docstring states it estimates deductions under Chapter VIA for the old regime, implying a bounded tax-deduction scope. However, the implementation later includes `24b_home_loan_interest_self_occupied` in both the total and output, which is not a Chapter VIA deduction and therefore contradicts the stated documentation.

Intent-Code Divergence

Medium
Confidence
98% confidence
Finding
The computed value is `total_tds - total_tax`, which is positive for a refund and negative for additional tax due, but the output label is phrased ambiguously enough to invert user interpretation. In a tax-estimation skill, this can directly mislead users about whether they owe tax or should expect a refund, causing incorrect filing or payment decisions.

Intent-Code Divergence

Medium
Confidence
98% confidence
Finding
The comparator reads an input FY but ignores it during tax calculation, always loading salary regime rules from FY-2026-27. In a tax-helper skill, this can produce materially incorrect advice for the user's actual filing year, especially where slab, rebate, deduction, or surcharge rules differ, causing users to underpay, overpay, or make incorrect declaration decisions.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
'--input', '/dev/stdin',
        '--rules', str(RULES)
    ]
    result = subprocess.run(
        cmd,
        input=json.dumps(payload),
        capture_output=True,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Intent-Code Divergence

Medium
Confidence
98% confidence
Finding
The test explicitly documents that the capital gains estimator returns tax contrary to the stated exemption logic, yet still treats the case as PASS. In a tax-helper skill, knowingly accepting incorrect tax computation can propagate false advice, understate or overstate liability, and mislead users into incorrect filings or compliance decisions.

Vague Triggers

Low
Confidence
91% confidence
Finding
The phrase 'Use when user has education loan or home loan and wants to understand tax treatment' describes a general situation rather than a specific invocation boundary. It does not provide explicit trigger phrases, exclusions, or negative examples, so the scenario could be selected for a wide range of ordinary tax questions involving loans.

Static analysis

No suspicious patterns detected.