GHSA-pr29-r6fv-grmq
ClassificationGuard certifies a contractor claim against employee facts (false VERIFIED via claim normalisation and unchecked facts)
Summary
ClassificationGuard.verify_classification_claim (worker classification under IRS common-law indicators) can certify the wrong worker status as VERIFIED, with a proof_ref. It is also reachable through TaxPreFlight.audit_transaction with action: "hire", which then returns allowed: true.
There are two independent causes.
- Claim normalisation by substring, with the employee check first. The free-text claim is upper-cased and then tested with
"W-2" in claim or "EMPLOYEE" in claimbefore"1099" in claim or "CONTRACTOR" in claim. A contractor claim that contains the word "employee" normalises toW2. That covers common forms such as "non-employee", "Non-Employee Compensation (1099-NEC)" and "not an employee". Against facts that the guard itself classifies as an employee (W2), such a claim matches and is certified. - Facts are read with
facts.get(key, False)and evaluated by truthiness. A missing fact counts as "no employee indicator", soverify_classification_claim("1099", {})derives1099and is certified. Non-boolean values are accepted, so the strings"no"/"false"count asTrue.
Impact
The guard exists to stop a worker who is an employee on the facts from being paid as a 1099 contractor, which means no withholding. An AI agent, or anyone who can influence the agent's output, can get a contractor classification certified for a worker the guard derives as an employee, just by phrasing the claim as "1099 non-employee contractor". The result carries status: VERIFIED and an authoritative proof_ref, so a downstream gate that follows the documented contract admits it.
Missing or wrongly typed facts are certified the same way when the guard is called directly. Through TaxPreFlight, missing facts are blocked by the required-field check, but string facts are not.
No code execution or data disclosure. The impact is on the integrity of the verification verdict.
Proof of concept
qwed-tax==0.2.0 from PyPI, Python 3.11:
from qwed_tax.guards.classification_guard import ClassificationGuard
from qwed_tax.verifier import TaxPreFlight
g = ClassificationGuard()
employee_facts = {"provides_tools": True, "reimburses_expenses": True, "indefinite_relationship": True}
# Control: plain "1099" against employee facts is correctly rejected
assert not g.verify_classification_claim("1099", employee_facts)["verified"]
# Contractor claims containing "employee" are certified against employee facts
for claim in ["1099 non-employee contractor",
"Non-Employee Compensation (1099-NEC)",
"Independent contractor (not an employee)"]:
r = g.verify_classification_claim(claim, employee_facts)
d = ClassificationGuard.to_diagnostic(r)
assert r["verified"] and d.status.value == "VERIFIED" and d.is_authoritative
# Missing facts and string facts are certified
assert g.verify_classification_claim("1099", {})["verified"]
assert g.verify_classification_claim("W-2", {"provides_tools": "no", "reimburses_expenses": "no",
"indefinite_relationship": "no"})["verified"]
# Through the documented entry point
TaxPreFlight().audit_transaction({"action": "hire",
"worker_type": "1099 non-employee contractor",
"worker_facts": employee_facts})
# -> {'allowed': True, 'blocks': [], 'checks_run': ['worker_classification'], ...}
Affected versions
qwed-tax (PyPI) <= 0.2.0. The same normalisation code is present in 0.1.0.
The npm package @qwed-ai/tax is not affected by this issue. Its TaxPreFlight.audit compares the claim as an exact string and rejects these phrasings.
Patches
Fixed in qwed-tax 0.2.1.
Workarounds
Until upgrading:
- Pass only the exact values
"W2"or"1099"as the claim, and reject any other claim text before calling the guard. - Validate that all three facts are present and are real booleans before calling
verify_classification_claim.
Fix
- Accept a closed set of claim values with an explicit alias table. Any other text, or text that matches both classes or neither, returns a non-verified
INVALID_CLAIMresult. - Require
provides_tools,reimburses_expensesandindefinite_relationshipto be present and of typebool; otherwise fail closed. - Regression tests cover the claims above and missing / non-boolean facts on both the direct API and
TaxPreFlight.