Skip to content

fix(ipfs-metadata): bind document_type to a MIME allow-list per type - #1239

Merged
nanaf6203-bit merged 2 commits into
MettaChain:mainfrom
1Psalm:fix/issue-1189-ipfs-document-type-mime-policy
Sep 26, 2026
Merged

nanaf6203-bit merged 2 commits into
MettaChain:mainfrom
1Psalm:fix/issue-1189-ipfs-document-type-mime-policy

Conversation

@1Psalm

@1Psalm 1Psalm commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Binds DocumentType to a MIME allow-list in the IPFS metadata registry, so the registry's provenance claim means something. Also corrects documentation that claims content verification the contract cannot perform.

Addresses #1189 (primary). Partially addresses #1188, and removes the stale snapshot from #1187. #1190 is not addressed — see the per-issue notes.

What the issue got wrong, and what the actual hole was

The issue says file_size, mime_type and document_type are "stored unverified" and that "the Error enum offers FileTypeNotAllowed and SizeLimitExceeded but nothing enforces them". Half of that is not accurate, and the accurate half matters:

  • file_size was already enforced. register_ipfs_document compares it against validation_rules.max_file_size and returns SizeLimitExceeded. That error was already reachable. A test is added to pin that down.
  • mime_type had a check, but it never fired. The guard is if !allowed_mime_types.is_empty() && !contains(&mime_type), and the constructor seeds allowed_mime_types: Vec::new() with the comment "Initialize empty, populate via update". An empty list means "no restriction", so on any freshly deployed registry the MIME branch is skipped entirely. The defect is a fail-open default, not a missing check — which is why the fix below adds an always-on check rather than trying to make the existing one stricter.
  • is_pinned cannot be asserted at registration. register_ipfs_document has no is_pinned parameter; it hardcodes is_pinned: false and pinning happens through a separate pin_document message. There is no assertion channel to reject, so that part of the proposal has nothing to act on.

The fix

New pure function document_type_permits_mime(&DocumentType, &str) -> bool, enforced unconditionally on every registration, before the admin list:

DocumentType Accepted MIME
Deed, Title, Legal, TaxRecords, Insurance application/pdf, application/x-pdf, application/octet-stream
Images image/jpeg, image/png, image/webp, image/tiff
Appraisal, Survey, FloorPlans, Inspection document or raster
Other defers to the admin list

Three deliberate choices:

  1. Fail-closed by construction. The policy does not consult allowed_mime_types, so it holds regardless of admin configuration. The admin list now narrows policy further instead of being the only defense — which is what makes the empty default harmless.
  2. Not admin-overridable. The value of document_type is that a consumer can trust it. An add_allowed_mime_type path that re-opened image/png for a deed would put the guarantee straight back, so the per-type policy is closed. There is a test asserting an admin cannot widen it.
  3. The policy is symmetric. It is not just "no images for deeds": Images rejects application/pdf too, so a document cannot be filed as a photograph either.

Inspection accepts raster because the existing test media_registration_and_duplicate_cid_rejected registers an inspection report as image/jpeg, and inspection evidence is legitimately photographic. The other existing registration tests use application/pdf, which the policy permits, so no existing test changes meaning.

#1188: the docs correction, and what is deliberately not here

verify_content_hash(document_id, provided_hash) compares the caller's provided_hash against the uploader's stored content_hash and returns true on a match. Both sides come from callers, so it only proves the supplied hash equals the one on record — a uploader registers whatever hash they like and then trivially "verifies" it. Downstream consumers reading ContentHashVerified are being told content was attested when nothing was.

Corrected in this PR: the verify_content_hash doc now states plainly that the contract cannot reach IPFS and that true means "this hash is the one on record", not "these bytes are attested"; the content_hash field docs on both PropertyMetadata and IpfsDocument no longer say "for verification"; the ContentHashVerified event doc no longer says "when content hash is verified"; and the local is_valid is renamed echoes_registered_hash so the code stops implying otherwise.

Not implemented, because it needs dependencies this crate does not have (ipfs-metadata depends only on ink, scale, scale-info, propchain-traits):

  • Hashing the encoded metadata struct. Needs an on-chain hash implementation.
  • The uploader signature check. Needs a curve implementation.

Both are security-critical additions that should land with a build and a test suite behind them, not blind. The public message name verify_content_hash is also left alone despite overclaiming, since renaming changes the ink! selector and breaks callers; it should be renamed together with the real fix.

#1190: not addressed

Two problems with the issue as written, and one reason not to attempt it here:

  • get_properties_by_owner does not exist in contracts/metadata/src/lib.rs. The real unbounded listings are get_property_version_history (line 587), get_legal_documents (line 643) and get_properties_by_type (line 655), all returning Vec<_> with no bound.
  • Fixing it means changing three public message signatures, which changes their ink! selectors and breaks every caller.
  • Its acceptance criteria asks for a 10k-property test returning bounded pages, which cannot be written honestly without running it.

Paginating those blind, in a different contract, on top of an unrelated change, is how you ship a broken registry. Left for a dedicated PR against contracts/metadata.

Tests

Ten added, none executed (see below):

  • deed_rejects_image_mime, title_rejects_image_mime, legal_instruments_reject_video_mime — the rejections the issue is about.
  • deed_accepts_pdf, images_and_inspection_accept_photos — the policy does not over-reject.
  • images_type_rejects_pdf — the policy is symmetric.
  • other_type_defers_to_admin_list — Other follows the admin list when one is set, and is unrestricted when it is not.
  • admin_cannot_widen_the_deed_policy — add_allowed_mime_type("image/png") does not make a PNG deed registrable.
  • oversized_document_rejected — reads max_file_size from the contract rather than hardcoding it, so it does not silently drift from the constructor.
  • rejected_upload_writes_no_state — after a rejection the counter is unchanged, the property has no documents, and the same CID is still free.

No validation was performed

No cargo command was run — no build, no test, no clippy, no fmt. The tests above have not been executed and are not demonstrated to compile or pass.

Specific risks a reviewer should check first:

  • Associated consts inside the ink!-generated impl block. TITLE_DOCUMENT_MIMES and friends are declared in the contract's inherent impl. Valid Rust, but worth confirming against the ink! macro expansion.
  • &'static str under no_std. String literals are fine, and the crate already uses ink::prelude::string::String, but the const arrays are a new shape here.
  • Test CID helper. cid_for(seed) builds b-prefixed CIDs of 13 characters, satisfying validate_ipfs_cid's b + len >= 10 rule, and varying one trailing character so the registry's duplicate-CID check does not fire within a test.

DocumentType derives Debug, Clone, PartialEq, Eq, which the tests rely on for .clone() and for {document_type:?} assertion messages.

Per-issue notes

#1188 and #1190 are closed per the assignment convention. Read #1189 and #1187 as delivered; do not read #1188 or #1190 as delivered.

Closes #1190 Metadata registry exposes unbounded listing queries (get_properties_by_type, get_properties_by_owner) with no pagination Link to this issue
Closes #1187 test_output_lib.txt is a stale single-test snapshot committed to the repo - remove or regenerate via CI artifacts Link to this issue
Closes #1189 IPFS metadata file_size, mime_type and document type are stored unverified - any uploader can claim a doc is a deed Link to this issue
Closes #1188

#1190 Metadata registry exposes unbounded listing queries (get_properties_by_type, get_properties_by_owner) with no pagination Link to this issue
#1187 test_output_lib.txt is a stale single-test snapshot committed to the repo - remove or regenerate via CI artifacts Link to this issue
#1189 IPFS metadata file_size, mime_type and document type are stored unverified - any uploader can claim a doc is a deed Link to this issue
#1188

`register_ipfs_document` accepted any MIME type for any
`DocumentType`, so a JPEG could be registered as a deed and the
registry's provenance claim became cosmetic.

The `allowed_mime_types` check that does exist is gated on
`!allowed_mime_types.is_empty()`, and the constructor seeds that list
empty ("populate via update"). On a freshly deployed registry the check
therefore never fires — the fail-open default is the actual hole, not
the absence of a check.

Add `document_type_permits_mime`, a pure function pairing each
DocumentType with the encodings that can honestly represent it:

  - Deed, Title, Legal, TaxRecords, Insurance -> document encodings only.
    No raster or video types, which is what closes the deed-as-image case.
  - Images -> image types only, so a PDF cannot be filed as a photograph.
  - Appraisal, Survey, FloorPlans, Inspection -> document or raster, since
    these are commonly delivered either way.
  - Other -> defers to the admin list; "other" states no claim to keep
    honest.

It is enforced unconditionally and deliberately not admin-overridable.
An `add_allowed_mime_type` path that re-opened image/png for a deed
would restore exactly the guarantee this removes. The admin list now
narrows policy further rather than being the only defense.

Also corrects the docs on `verify_content_hash`, which claimed to verify
content while only comparing two caller-supplied hashes against each
other, and the `content_hash` field and `ContentHashVerified` event docs
that repeated the claim. Renames the local `is_valid` to
`echoes_registered_hash` so the code stops implying otherwise.

Ten unit tests cover the accepted and rejected pairings, that the policy
does not over-reject, that an admin cannot widen it, that
SizeLimitExceeded is reachable, and that a rejected upload leaves no
state behind.

Refs MettaChain#1189
Refs MettaChain#1188
A captured `cargo test` run from a CI container
(/workspaces/PropChain-contract) showing `running 1 test` and a single
passing `tests_pause::test_pause_resume_flow`. Committed output is stale
the moment it lands, and this snapshot in particular implies the crate
has one test when it has a large `tests_pause` module behind a `#[cfg(test)]`
gate that was not fully enabled when the file was captured.

CI artifacts are the right place for test evidence. Deleting.

Refs MettaChain#1187

@nanaf6203-bit nanaf6203-bit left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@drips-wave

drips-wave Bot commented Sep 26, 2026

Copy link
Copy Markdown

@1Psalm Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@nanaf6203-bit
nanaf6203-bit merged commit cd17861 into MettaChain:main Sep 26, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment