Skip to content

Cross-feed conflict resolution trusts unverified generatedAt; no rollback detection #2

Description

@petfold

Labels: bug


Two related defects in the multi-feed union, both independent of whether
manifest signatures get verified.

1. One feed can shadow the entire union

loadIndexes() sorts indexes by manifest.generatedAt and then lets later
entries overwrite earlier ones per key (src/lib/index-api.js:132-138):

indexes.sort((a, b) => a.manifest.generatedAt - b.manifest.generatedAt);
for (const index of indexes) {
  for (const repo of index.repos) repoMap.set(repo.rid, repo);
  for (const user of index.users) userMap.set(user.did, user);
}

generatedAt is an unauthenticated integer from the fetched document. A feed
claiming a large generatedAt wins every per-RID and per-DID conflict, so a
single feed in the user's list controls the displayed name, description, and
activity of every repo the others also carry. Since users add feeds themselves
(setIndexFeeds, :29), the failure mode is "adding a second feed silently
replaced my first one's data", with no indication in the UI.

Worth considering: surface per-key provenance (which feed a row came from) and
treat conflicts as presentable ambiguity rather than something to silently
resolve. At minimum, don't let an unverified field decide it.

2. A stale-but-genuine update is undetectable

Canopy fetches bzz://<feedRef>/… (:72-80) and never sees the feed's update
index, so it cannot tell a current update from an older genuine one replayed by
whatever served it. Signature verification does not fix this — a rolled-back
update is correctly signed.

There's in-org precedent for the fix. freedom-adblock-service specifies
downgrade protection in its manifest contract (src/manifest.ts:19):

clients MUST reject a manifest whose version is <= the one they have already
applied (downgrade protection)

…and has clients verify a per-blob sha256 before trusting it (:20-21). The
index protocol has no equivalent, and canopy has no equivalent. Adopting the
sibling service's rule would be consistent rather than novel.

Suggested mitigation: resolve feeds explicitly so the update index is visible
(resolveFeed() in swarmlite's js/src/feeds.js returns { reference, index }),
persist the highest index seen per feed, and refuse to regress. That turns an
undetectable rollback into a visible, reportable staleness condition. It is a
best-effort defense, not a proof — js/src/feeds.js:38-44 is explicit that no
feed reader can prove latestness client-side, and canopy's UI should say so
rather than imply freshness it can't establish.

Happy to PR the monotonicity check alongside the signature work.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions