---
title: "GHSA-pr29-r6fv-grmq"
description: "ClassificationGuard certifies a contractor claim against employee facts (false VERIFIED via claim normalisation and unchecked facts). Affected and fixed versions of qwed-tax."
url: https://qwedai.com/security/advisories/ghsa-pr29-r6fv-grmq
---

# 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.

1.  **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 claim` before `"1099" in claim or "CONTRACTOR" in claim`. A contractor claim that contains the word "employee" normalises to `W2`. 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.
2.  **Facts are read with `facts.get(key, False)` and evaluated by truthiness.** A missing fact counts as "no employee indicator", so `verify_classification_claim("1099", {})` derives `1099` and is certified. Non-boolean values are accepted, so the strings `"no"` / `"false"` count as `True`.

### 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:

```python
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_CLAIM` result.
-   Require `provides_tools`, `reimburses_expenses` and `indefinite_relationship` to be present and of type `bool`; otherwise fail closed.
-   Regression tests cover the claims above and missing / non-boolean facts on both the direct API and `TaxPreFlight`.
