You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Recipe writes accept an arbitrary field set with no server/service-side allowlist or schema enforcement. Two entry points feed unvalidated objects through to the live recipe document:
RecipeEditSuggestionService.create() stores caller-supplied proposedChanges almost verbatim (only images/mediaInstructions/toDelete/mediaToDelete are separated out).
buildApplyPayload() (recipe-diff-utils.js) spreads ...changes (everything except those 4 keys) into the apply payload.
RecipeService.update() writes docPayload = { ...stripUndefined(changes) } directly to recipes/{id} — any key in changes lands on the document.
The edit-suggestion review UI only renders a fixed set of fields (META_FIELDS + ingredients/instructions/comments/images/media/related). Any field outside that set is applied on approval but never shown to the manager — e.g. userId (which the recipes update rule keys ownership on), approved, featured, or arbitrary junk keys.
Severity / why this is deferred, not urgent
The only way to inject rogue fields today is a direct Firestore console write — the recipe form (buildRecipeData()) can only produce the known field set, so the normal UI-driven path is safe. The Firestore create rule already pins suggestedBy and status == 'pending', but rules can't practically validate nested object shapes. There is also no canonical recipe schema in the codebase to allowlist against (validateRecipeData() only checks required-field presence, doesn't strip unknown keys, and isn't called in the service layer), so a hand-maintained allowlist would risk silently dropping valid fields — worse than the current state.
Proposed work
Define one canonical recipe field schema (single source of truth) covering the fields buildRecipeData() produces: name, description, category, difficulty, mainIngredient, prepTime, waitTime, servings, servingsUnit, tags, ingredients, ingredientSections, instructions, stages, images, mediaInstructions, comments, attribution, relatedRecipes (+ the transient toDelete / mediaToDelete contract keys).
Enforce it in the service layer (not just the form):
RecipeService.update() — sanitize changes to the schema allowlist before writing; drop/reject unknown keys.
RecipeEditSuggestionService.create() — validate/sanitize proposedChanges to the same schema before persisting, so a suggestion can never carry fields outside the editable surface.
Decide drop-vs-reject behavior (silently strip unknown keys, or throw) and apply consistently.
Add unit tests: "unknown fields in proposedChanges / changes are not written to the recipe."
Acceptance criteria
A single schema/allowlist module is the source of truth for editable recipe fields.
Both RecipeService.update() and RecipeEditSuggestionService.create() enforce it.
A crafted object with extra keys (e.g. userId, approved) cannot reach recipes/{id} via either path.
Problem
Recipe writes accept an arbitrary field set with no server/service-side allowlist or schema enforcement. Two entry points feed unvalidated objects through to the live recipe document:
RecipeEditSuggestionService.create()stores caller-suppliedproposedChangesalmost verbatim (onlyimages/mediaInstructions/toDelete/mediaToDeleteare separated out).buildApplyPayload()(recipe-diff-utils.js) spreads...changes(everything except those 4 keys) into the apply payload.RecipeService.update()writesdocPayload = { ...stripUndefined(changes) }directly torecipes/{id}— any key inchangeslands on the document.The edit-suggestion review UI only renders a fixed set of fields (
META_FIELDS+ ingredients/instructions/comments/images/media/related). Any field outside that set is applied on approval but never shown to the manager — e.g.userId(which the recipesupdaterule keys ownership on),approved,featured, or arbitrary junk keys.Severity / why this is deferred, not urgent
The only way to inject rogue fields today is a direct Firestore console write — the recipe form (
buildRecipeData()) can only produce the known field set, so the normal UI-driven path is safe. The Firestore create rule already pinssuggestedByandstatus == 'pending', but rules can't practically validate nested object shapes. There is also no canonical recipe schema in the codebase to allowlist against (validateRecipeData()only checks required-field presence, doesn't strip unknown keys, and isn't called in the service layer), so a hand-maintained allowlist would risk silently dropping valid fields — worse than the current state.Proposed work
buildRecipeData()produces:name, description, category, difficulty, mainIngredient, prepTime, waitTime, servings, servingsUnit, tags, ingredients, ingredientSections, instructions, stages, images, mediaInstructions, comments, attribution, relatedRecipes(+ the transienttoDelete/mediaToDeletecontract keys).RecipeService.update()— sanitizechangesto the schema allowlist before writing; drop/reject unknown keys.RecipeEditSuggestionService.create()— validate/sanitizeproposedChangesto the same schema before persisting, so a suggestion can never carry fields outside the editable surface.Acceptance criteria
RecipeService.update()andRecipeEditSuggestionService.create()enforce it.userId,approved) cannot reachrecipes/{id}via either path.References
src/js/services/recipes/recipe-service.js(update, ~L253–371)src/js/services/recipes/recipe-edit-suggestion-service.js(create)src/js/utils/recipes/recipe-diff-utils.js(buildApplyPayload)src/lib/recipes/recipe_form_component/recipe_form_component.js(buildRecipeData, ~L313) — de-facto current field sourcesrc/js/utils/recipes/recipe-data-utils.js(validateRecipeData) — presence-only, to be extended/superseded