• v0.9.1 2723baa472

    v0.9.1
    All checks were successful
    CI / gate (push) Successful in 1m4s
    CI / image (push) Successful in 51s
    Stable

    joeyr released this 2026-08-10 20:20:00 -05:00 | 89 commits to main since this release

    Two gaps between a guarantee this project had written down and what the code actually
    enforced. Both were found by a read-only audit of 0.9.0 (docs/audit.md), neither by a
    failing test — the suite was green through both.

    Fixed

    • An event_id could name a path, not just a filename. calendar/eventid.py argues
      at length that the handle does not need signing, because a forged one names a calendar
      and WRITABLE_CALENDARS is checked on that name before iCloud is contacted. That
      argument holds only while the handle's resource field contributes a filename — and
      nothing checked. A hand-built segment of ../Private/target.ics was joined onto the
      allowlisted collection URL verbatim, and caldav's URL.canonical() does not remove dot
      segments, so the traversal went on the wire for iCloud to normalise. Reachable for both
      update_event and delete_event, and the confused deputy is realistic: event titles
      from a subscribed feed or an emailed invite land in the model's context, and inducing a
      tool call with a crafted argument is the standard shape of that attack.

      decode() now refuses a separator, a dot segment, or either one percent-encoded — the
      check runs against the unquoted form so the encodings do not have to be enumerated,
      while a double-encoded %252f still decodes as the filename character it is. The
      precondition is now recorded in the module docstring next to the argument that depends
      on it, rather than left implicit.

      The tests could not have caught this, which is worth recording separately: the fake
      at tests/test_client.py resolved an event by taking the last segment of whatever URL
      it was handed, throwing the traversal away. A test asserting the refusal would have
      passed against it no matter what the code did. The fake now rejects anything that is not
      the collection URL plus one segment, so the new refusal cases prove something.

    • The identity allowlist only guarded tool calls. IdentityAllowlist implemented
      on_call_tool and nothing else, so any account on the Forgejo instance — which is every
      account, since Forgejo has no OAuth scopes and that is the whole reason
      ALLOWED_SUBJECTS exists — could complete the flow, initialize a session and read the
      server instructions, which name the writable calendars. Calendar names are precisely
      the value the 2026-08-08 history rewrite was performed to remove.

      The hook is now on_message, which covers initialize, the listings and calls alike.
      on_call_tool is kept as a second check behind it, so a change in how FastMCP
      dispatches middleware cannot silently reopen the write surface.

    Downloads