|
| 1 | +--- |
| 2 | +'@objectstack/plugin-security': minor |
| 3 | +--- |
| 4 | + |
| 5 | +fix(plugin-security)!: the record an anonymous public-form submit echoes back passes the result masker, so a field whose masking rule applies is echoed masked (#21062) |
| 6 | + |
| 7 | +Clause-②: no (narrowing) |
| 8 | + |
| 9 | +<!-- adr-0087: not-required (no-migration-prescription) Nothing an author wrote is rewritten or changes meaning: every maskingRule already declared that it applies to every non-system caller who does not hold all of the field's requiredPermissions, and an anonymous form submitter is such a caller. The runtime now honours that declaration on the one door that skipped the masker. No stored metadata, schema key or published type moves. --> |
| 10 | + |
| 11 | +**BREAKING for the anonymous public-form submit's response.** |
| 12 | + |
| 13 | +**What changed.** A public form's submission is authorized by the ADR-0056 |
| 14 | +declaration-derived grant, which admits the create and the read-back on the |
| 15 | +form's declared object and passes before any permission set is resolved. The |
| 16 | +grant handed the operation to the engine and returned before the result masker |
| 17 | +ran, so the record echoed in the `201` body carried every field whose |
| 18 | +`maskingRule` applies as stored: the field the form collects, and a field |
| 19 | +filled from its `defaultValue` that the form never shows. |
| 20 | + |
| 21 | +The grant now hands what it returns to the same result masker the data plane |
| 22 | +uses, for the caller it stands in for: the permission sets resolved for the |
| 23 | +grant's context (the deployment's guest set when it registers one, otherwise |
| 24 | +none) and the object posture those sets read. A field whose masking rule |
| 25 | +applies is echoed masked. A field the caller's sets mark unreadable, or whose |
| 26 | +`requiredPermissions` they do not hold, is masked the way the data plane masks |
| 27 | +it for that caller. The read-backs the grant admits are masked the same way. |
| 28 | + |
| 29 | +**What did not change.** The grant admits exactly what it admitted: the create |
| 30 | +and the read-back on the form's declared object, and nothing else. The |
| 31 | +server-managed fields are still stripped from the submitted row. The stored row |
| 32 | +is unchanged; only the echo is masked. |
| 33 | + |
| 34 | +**One more refusal, by the same rule.** If the caller's permission sets or the |
| 35 | +object's security posture cannot be read, the submission is now refused with |
| 36 | +`403 PERMISSION_DENIED` before anything is written, as every other caller's |
| 37 | +request already is. The masking rules come from that posture, so the echo |
| 38 | +cannot be masked without it. |
| 39 | + |
| 40 | +**What to do.** Nothing, unless a client reads a masked field's stored value |
| 41 | +back out of the submit response. The response now carries the masked value, as |
| 42 | +every other non-system read does. A field's `requiredPermissions` are the gate |
| 43 | +that lifts its mask, so a deployment whose guest set holds all of them is |
| 44 | +echoed the stored value; otherwise drop the `maskingRule`. |
0 commit comments