Skip to content

meta-plural-url-bypass: PUT /meta/fields/<name> walks around the whole two-tier registry gate — 4 registry types have no entry in PLURAL_TO_SINGULAR #7894

Description

@huangyiirene

Summary

canonicalMetaType — the ONE canonical spelling of a metadata type at the /meta read/write/delete boundary (#4432) — folds plural→singular through PLURAL_TO_SINGULAR. That map is a manifest collection map (objects: [...], apps: [...]), and it is missing entries for four registry types. For those, the plural URL spelling is not folded: it is treated as an unregistered plugin type, which every authorization gate treats as permissive by construction.

Result: the plural URL is a door around the singular URL's lock.

Measured, live (booted showcase, admin bearer, on top of #7743's fix)

PUT /api/v1/meta/field/showcase_task.title → 403 NOT_OVERRIDABLE ← #7743's fix, correct
PUT /api/v1/meta/fields/showcase_task.title → 200 {"success":true,"state":"active",
"message":"Saved fields 'showcase_task.title' …"}

Note the receipt: Saved fields. The row is persisted with type='fields' — a second namespace for the same item, which is precisely the defect class #4432 was filed about ("one plural-spelled read minted a plural registry entry … one overlay row shadowed an entire code-authored listing, and survived the DELETE that was supposed to lift it").

Why the gates let it through

For an unmapped spelling 'fields':

Every one of those is correct behaviour for a genuinely plugin-registered type. The bug is that 'fields' is not one — it is a registry type wearing a spelling the boundary map does not know.

Blast radius (measured against DEFAULT_METADATA_TYPE_REGISTRY at bcea363)

Registry types with no singular target anywhere in PLURAL_TO_SINGULAR:

typeallowOrgOverrideallowRuntimeCreateexploitable today
fieldfalsetrueyes — proven above; artifacts exist (inside objects)
seedfalsetruestructurally, if a package ships one
external_catalogfalsetruestructurally, if a package ships one
translationtruetrueno — the singular route allows it anyway

Plus email_template, reachable only as camelCase emailTemplates; the REST-shaped /meta/email_templates is unmapped. (allowOrgOverride: true, so no lock to bypass.)

So the one type where this is a live authorization hole today is field, which is why it surfaced under #7743.

Why #7743 did not fix it

It is a different defect with a wider remedy, and the narrow patch is the wrong shape. Two candidate fixes, both needing a decision:

  1. Add the missing keys to PLURAL_TO_SINGULAR.⚠️ Careful: that map is also iterated by metadata-authoring-lint.ts to emit "did you mean" hints for stack-level manifest collections. Adding fields: there would advertise a top-level fields: [...] collection that does not exist and collides conceptually with ObjectSchema.fields.
  2. Split the two roles — a manifest-collection map and a URL-spelling map are two different contracts that happen to overlap. canonicalMetaType would read the URL one, derived from DEFAULT_METADATA_TYPE_REGISTRY so a new type cannot arrive unmapped (Prime Directive Convert to monorepo with scoped packages #8: registry-derived, never a hand-written list).

A third option worth pricing: refuse an unknown plural at the boundary rather than silently treating it as a plugin type — an unmapped spelling of a declared type is a caller error, and answering 200 to it is how the second namespace gets minted.

⛔ Do not fix this by teaching isNestedArtifactField (or any other predicate) to accept 'fields' — that is a spelling-tolerant lookup one layer below the boundary, which is the exact pattern #4432's doc comment rejects, and it would still persist the row under type='fields'.

Repro

  1. Boot the showcase with writable runtime packages; obtain an admin bearer.
  2. PUT /api/v1/meta/field/showcase_task.title {name:'title', label:'X', type:'text'} → 403 (with org-override-registry-gate: the field overlay lock is not enforced — an artifact-backed field PUT is accepted 200 (and is inert) #7743's fix).
  3. PUT /api/v1/meta/fields/showcase_task.title with the same body → 200.
  4. SELECT type, name FROM sys_metadata → a row with type='fields'.

Source

Found while writing #7743's route-level pin: the plural case was written expecting a 403 and measured 200, then confirmed against the live showcase. Not present in #7743's original report.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions