Skip to content

v0.4.0

Latest

Choose a tag to compare

@github-actions github-actions released this 07 Sep 13:22
· 5 commits to main since this release
v0.4.0
1bc0594

Security

  • The image scan of import_recipe_from_html_or_json is one pass over the
    document, and it decodes what Mealie decodes.
    A document of "image":[
    repeated to the 2 MiB limit cost 223 seconds on the thread that serves every
    request: a 4096-character window was sliced, searched and URL-parsed per key.
    The scan now reads the document as JSON when it parses (so "\u0069mage" is
    image, as it is to Mealie), as JSON text with string escapes decoded (so
    "http:\/\/…" — what PHP's json_encode writes by default — is the address
    it decodes to, where a reader that stopped at the backslash saw http: and
    dropped the candidate), and as HTML with character references decoded (so
    100.100.100.200 is 100.100.100.200, where the host used to be &).
    Both bypasses were reproduced against the metadata address Mealie's own
    guard does not stop. It also reads <meta itemprop="image"> and
    <link rel="image_src">, which extruct reads and the scan did not; refuses
    an absolute address it cannot parse rather than passing it on, since "cannot
    parse" is not "harmless" when the next parser is somebody else's; and refuses
    a document naming more than 25 hosts or carrying more than 500 image
    references, where the 26th host used to be skipped in silence. A timing table
    holds every form at the size limit under half a second.
  • MEALIE_API_TOKEN and MEALIE_ACCEPT_LANGUAGE are checked for shape at
    startup, and every header value before a request.
    undici refuses a header
    value with a control character in it by quoting the whole value in its error
    — for Authorization, the whole value is the token — and that error reached
    the model as the tool result. A token with a line break inside it (the way a
    wrapped paste arrives) now ends the process with a message that names the
    variable and its length, and never the value; a language that is not a
    language range is dropped with the same care. A property test drives random
    tokens with control characters through the whole path and asserts the result
    text never carries them.
  • Every string the instance wrote is cleaned before it is shown. C0 and C1
    controls, DEL, the zero-width set, the BiDi overrides and the byte-order mark
    are removed from every field the projections carry and from the whole object
    in get_recipe's raw mode, preview_recipe_url, parse_ingredients and
    set_recipe_rating, which used to hand Mealie's object on untouched. Recipes
    are scraped from arbitrary websites; an escape sequence in a step arrived
    intact. Identifiers, dates and URLs are validated instead: an id that is not
    shaped like one is dropped, and the credentials in a stored source URL are
    redacted.
  • Fields named like a credential are redacted at any depthpassword,
    secret, token, api_key, private_key, passphrase, matched on the
    suffix of the normalised name. Mealie's extras is arbitrary key/value data
    written by integrations, and it travelled through the raw passthroughs as
    written.
  • create_cookbook binds the confirmation to everything its dialog names.
    The consequence said "its name, description and saved filter become readable
    outside the instance" and the resource key held the name alone, so a token
    issued for one description executed with another. The key now covers all
    three, in order, and the dialog shows all three. create_share_token and
    merge_foods/merge_units build their ordered keys with the library's
    orderedResourceKey instead of a hand-written join, and
    update_shopping_list_items binds the list its sentence names.
  • The status is decided before the body is read. A 401 behind a reverse
    proxy that answers with a login page of megabytes surfaced as "more than the
    byte limit" — the size, not the status, and no hint about credentials. An
    error body is now read under its own 64 KiB ceiling that cuts instead of
    refusing; the 8 MiB cap applies to a success body only.
  • What resolveRecipe hands on is validated, not merely typed. The id and
    the slug are the instance's strings, and they went on into a request path, a
    query-filter literal (recipe_id="…") and the sentence a person is asked to
    approve; a " in the id would have broken out of the filter. The id must be
    a UUID and the slug a slug, or the recipe is reported as unrecognisable.
    The same for the user id set_recipe_rating puts in a path.
  • SECURITY.md described a transport this server no longer uses. The
    "binding is not freshness" section argued that protocol revision
    2026-07-28 was unreachable and that no anti-replay mechanism was therefore
    needed — while serveStdio negotiates that revision and mcp-approval 0.8.1
    made a sealed answer single-use. The section now describes what the code does.
  • Supply chain. The release job installs with --ignore-scripts, as the
    audit job and the Dockerfile already did — it is the job that holds the OIDC
    token; mcp-publisher is pinned to v1.8.1 and its published checksum instead
    of releases/latest; gh release create verifies the tag; pull requests get
    dependency-review-action at fail-on-severity: high; the runtime image no
    longer carries yarn, corepack or the lockfile; the integration job's checkout
    keeps no credentials.
  • mcp-approval 0.8.2. A sealed dialog answer is single-use since 0.8.1: the
    same requestState presented again within its lifetime used to be accepted
    again, and with a resource key that is the same every time — a whole stream,
    a fixed set of targets — every replay landed. npm users on ^0.8.0 already
    had the fix; the Docker image is built from the lockfile and carried 0.8.0
    until this release.

Fixed

  • Organizer lookups have a budget per call. search_recipes resolves up to
    sixty names and update_recipe up to a hundred, one or two requests each in
    sequence, and the fifteen-second timeout bounded each request rather than the
    call. A wall clock of thirty seconds is checked before every request; past it
    the call says how far it got and what to narrow.
  • A single oversized field no longer makes a recipe unanswerable. A 300 kB
    calories — schema.org nutrition is text Mealie takes from the page — left
    nothing array-shaped for the budget to shrink, so get_recipe threw. Every
    projected field is bounded, raw strings are cut at twenty thousand characters,
    and the budget shrinks the array that is largest in bytes rather than in
    elements, which used to halve a list of three hundred tag names to nothing
    while forty long instructions stayed.
  • The text block of a marked result stays under the cap with the untrusted
    preamble included; the budget measured the JSON alone.
  • A truncated key the instance carries cannot replace the budget's own notice
    or fail the answer against its schema; it is dropped alongside untrusted
    and source.
  • A list of names — tags, categories, aliases, missing foods — no longer
    carries undefined for a reference without a name, which the text block
    wrote as null and the structured half did not. A property test now feeds
    every read tool arbitrary JSON, including 1e999, -0 and a __proto__
    key, and asserts both channels of each answer agree and no answer reads
    "Output validation error" or "Cannot read properties".
  • Error bodies and runtime error messages quoted into a result are stripped of
    control characters, cut, and labelled as the instance's words.
  • confirm_token, search_recipes.cookbook and every ISO timestamp argument
    are bounded; the token past 512 characters was compared before it was refused.
  • MEALIE_URL is stored as it was checked — the parsed origin and path — rather
    than as the raw string, and the trailing-slash trim is a loop rather than an
    unanchored /\/+$/.
  • The ELICITATION diagnostic describes a long value by its length instead of
    quoting it, and the wrong-scheme diagnostic for MEALIE_URL no longer prints
    the scheme: a hexadecimal key with a colon after it is a valid URL whose
    scheme is the key.
  • get_shopping_list, update_shopping_list_items, update_mealplan_entry
    and the organizer lookups read a null or non-object body as an empty record
    instead of answering "Cannot read properties of null".
  • get_about answers with scalars only — the group and household as names, not
    the whole objects — and cleans the instance's strings before they travel
    unmarked.
  • publishConfig.access is now public. This package is scoped, and npm
    publishes a scoped package as restricted unless told otherwise — every other
    scoped server in the family carried the field and this one did not. The
    published versions are unaffected: npm keeps the visibility a package already
    has, so this closes a hole that would only have opened on a first publish
    under a new name.

Added

  • The server introduces itself in full. title, description, websiteUrl and
    icons now travel with name and version, so a client that shows a server
    to a person has something to show. All four were already in server.json for
    the registry and reached no client at all; a test compares the two so they
    cannot drift.
  • Server instructions. Results carry an untrusted marker, but that is read
    after the fact — this is the channel a model sees before it calls anything.
  • An OpenSSF Scorecard run, weekly and on every push to main, reporting into
    the Security tab next to CodeQL and Trivy. The badge is the second in the row.

Changed

  • The tool reference marks the essential preset and the tools that ask a
    person before they act, per tool rather than only in the introduction. A test
    keeps both sets in step with the code.
  • Source maps are no longer published in the npm tarball. Node reads them only
    under --enable-source-maps, which nothing here sets, and the maps pointed at
    a src/ this package does not ship — so a stack trace under that flag named a
    file nobody could open. dist/**/*.js is unchanged; the package is about a
    fifth smaller.