Found while surveying which platform rules fire on objectstack-ai/hotcrm (its #1621, step 3 of epic #1579). Read-only survey, report-first, unassigned.
Measured against the pinned @objectstack/lint@17.3.0, hotcrm at 1670557. Both injections proven on disk; both restored by blob hash verified by observation.
The pair, same key, same file, same run
src/profiles/sales-rep.profile.ts authors fields: { 'crm_account.description': { readable: true, editable: true }, … }.
| injection |
objectstack lint |
exit |
key to 'description' (unqualified) |
security-fls-unqualified-key at error |
1 |
key to 'crm_account.description_nope' (qualified, field does not exist) |
passed: true, no new finding |
0 |
The second row is the finding; the first is its working control — same file, same harness, same command, one run apart.
Why the silent half is the worse one
The rule's own message explains that FLS is matched at runtime by the object.field prefix, so an unqualified key is silently ignored and the declared masking never enforces. A qualified key naming a field that does not exist has the identical runtime consequence — nothing ever matches it — but it looks correct in review, survives rename refactors invisibly, and is exactly what a field rename leaves behind. It is the shape that accumulates.
On hotcrm the local assertion test/authorization-coverage.test.ts → every FLS key is object-qualified and names a real field goes red on both injections, which is how the asymmetry surfaced: that assertion cannot be retired against security-fls-unqualified-key, because the rule covers only half of what it asserts.
Adjacent precedent
#15495 (closed) — "The object write door still runs no field-existence rule for searchableFields or listViews — the same asymmetry #15254 closed one key over". Same shape of gap (a declared key whose field is never resolved), one surface over. Cited as precedent, not as a duplicate: it does not reach permission-set fields.
Dedupe
Targeted search over this repo returned 10 on-topic results (#14747, #15495, #12935, #11000, #9538, #9327, …), none naming permission-set field-permission keys. That non-zero, on-topic return is the control that the search was not silently answering empty.
Found while surveying which platform rules fire on
objectstack-ai/hotcrm(its #1621, step 3 of epic #1579). Read-only survey, report-first, unassigned.Measured against the pinned
@objectstack/lint@17.3.0, hotcrm at1670557. Both injections proven on disk; both restored by blob hash verified by observation.The pair, same key, same file, same run
src/profiles/sales-rep.profile.tsauthorsfields: { 'crm_account.description': { readable: true, editable: true }, … }.objectstack lint'description'(unqualified)security-fls-unqualified-keyaterror'crm_account.description_nope'(qualified, field does not exist)passed: true, no new findingThe second row is the finding; the first is its working control — same file, same harness, same command, one run apart.
Why the silent half is the worse one
The rule's own message explains that FLS is matched at runtime by the
object.fieldprefix, so an unqualified key is silently ignored and the declared masking never enforces. A qualified key naming a field that does not exist has the identical runtime consequence — nothing ever matches it — but it looks correct in review, survives rename refactors invisibly, and is exactly what a field rename leaves behind. It is the shape that accumulates.On hotcrm the local assertion
test/authorization-coverage.test.ts→ every FLS key is object-qualified and names a real field goes red on both injections, which is how the asymmetry surfaced: that assertion cannot be retired againstsecurity-fls-unqualified-key, because the rule covers only half of what it asserts.Adjacent precedent
#15495 (closed) — "The object write door still runs no field-existence rule for searchableFields or listViews — the same asymmetry #15254 closed one key over". Same shape of gap (a declared key whose field is never resolved), one surface over. Cited as precedent, not as a duplicate: it does not reach permission-set
fields.Dedupe
Targeted search over this repo returned 10 on-topic results (
#14747,#15495,#12935,#11000,#9538,#9327, …), none naming permission-set field-permission keys. That non-zero, on-topic return is the control that the search was not silently answering empty.