Skip to content

feat: safer-defaults helpers and unify EXIF orientation parsing#16

Merged
lilith merged 4 commits intomainfrom
fix/medium-hardening-2026-05-06
May 7, 2026
Merged

feat: safer-defaults helpers and unify EXIF orientation parsing#16
lilith merged 4 commits intomainfrom
fix/medium-hardening-2026-05-06

Conversation

@lilith
Copy link
Copy Markdown
Member

@lilith lilith commented May 6, 2026

Addresses MEDIUM findings from the 2026-05-06 security audit. All changes are additive or internal — public type fields and method signatures are unchanged. ResourceLimits::default() continues to return all-None for backwards compatibility; the new for_untrusted_input() helper is the recommended path going forward.

Findings addressed

M1 — ResourceLimits::default() is no-limits

Adds ResourceLimits::for_untrusted_input() (alias safe_default()) with sane caps for processing untrusted input:

  • max_pixels: 100 MP per frame
  • max_total_pixels: 200 MP across an animation
  • max_width / max_height: 16384 each
  • max_memory_bytes: 1 GiB
  • max_input_bytes: 256 MiB
  • max_frames: 65 536
  • max_animation_ms: 1 hour

ResourceLimits::default() is unchanged — additive helper, no breaking change. 6 new tests verify the caps reject oversized input, accept typical 4K/12 MP images, and that default() still has no caps.

M2 — DecodePolicy::strict() not visibly promoted

Module-level docs in policy.rs and the DecodePolicy struct doc now recommend DecodePolicy::strict() as the starting point for untrusted input, paired with ResourceLimits::for_untrusted_input. No code changes.

M3 — Duplicated EXIF orientation parsing

metadata::parse_exif_orientation was a looser duplicate of helpers/exif.rs::parse_exif_orientation:

  • It only read the orientation value as u16 regardless of the TIFF type field, missing TIFF_LONG (type 4) values for big-endian inputs.
  • It lacked the IFD entry-count cap (DoS protection) and tag-sort early-exit present in the helper.

Now delegates to the canonical implementation. 2 new tests verify TIFF_LONG (BE + LE) is now handled correctly through the metadata module path.

M4 — Dyn-job setters silently no-op after consumption

DynDecodeJob and DynEncodeJob shim setters silently no-op'd if invoked after the inner job had been moved out by an into_* method. This path is structurally unreachable from external code (every into_* takes Box<Self>) but the silent fallback masked any future regression.

The setters now route through a try_apply helper that fires debug_assert! on the consumed-after path, catching misuse loudly in tests and dev builds. Release behaviour is unchanged (silent no-op preserved). Trait signatures are unchanged — making the setters fallible would break the public API of every codec implementing the trait, which the audit explicitly told this PR to avoid.

Validation

  • cargo build (host + wasm32-unknown-unknown --no-default-features)
  • cargo fmt --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test — 432 unit + 100 integration + 29 integration (other suites) + 14 doctests, all passing

Test plan

  • New tests for for_untrusted_input rejection of oversized images and inputs
  • New tests for parse_exif_orientation accepting TIFF_LONG type (BE + LE)
  • All existing tests pass
  • cargo build --target wasm32-unknown-unknown --no-default-features succeeds (no_std + alloc preserved)

lilith added 4 commits May 6, 2026 05:00
…helper

Introduce ResourceLimits::for_untrusted_input() (alias safe_default()) with
sane caps for processing untrusted input: 100 MP per frame, 200 MP total,
16384×16384 max dims, 1 GiB memory, 256 MiB input, 65536 frames, 1h duration.

ResourceLimits::default() continues to return all-None (no limits) for
backwards compatibility — the new helper is the recommended starting point
for services accepting bytes from the network or end users.

Adds 6 tests verifying the caps reject oversized input, accept typical
4K/12 MP images, and that default() behavior is unchanged.
The metadata.rs orientation parser was a looser duplicate of helpers/exif.rs
— it only read u16 at +8 regardless of TIFF type field, missing TIFF_LONG
(type 4) values that store data in different bytes for big-endian, and it
lacked the IFD entry-count cap and tag-sort early exit that helpers/exif.rs
provides as DoS protection.

Have metadata::parse_exif_orientation delegate to the canonical impl in
helpers/exif.rs. Adds two tests verifying TIFF_LONG type is now handled
correctly (BE and LE) — the previous parser silently missed those values.
The DynDecodeJob and DynEncodeJob shims silently no-op'd if a setter was
called after the inner job was moved out by an into_* method. This is
unreachable through the public API (every into_* method takes Box<Self>),
but the silent fallback masked the bug if the path were ever reached
through internal misuse or a future refactor.

Consolidate the setter bodies through a try_apply helper that fires
debug_assert! when the inner job is missing, so any future regression
that exposes the consumed-after path fails loudly in tests and dev
builds. Release-build behaviour is unchanged (silent no-op preserved)
to avoid any chance of a panic in production code.

The DynDecodeJob/DynEncodeJob trait signatures are unchanged — these
remain infallible setters for backwards compatibility per the audit's
guidance to avoid breaking changes to public types in this foundation
crate.
@lilith lilith self-assigned this May 6, 2026
@lilith
Copy link
Copy Markdown
Member Author

lilith commented May 7, 2026

@lilith — ready for review. Part of the 2026-05-06 security audit campaign (see /home/lilith/work/feedback/security-audit-2026-05-06/FIX-RESULTS.md and REVIEW-RESULTS.md).

@lilith lilith merged commit 81f9dd6 into main May 7, 2026
14 checks passed
@lilith lilith deleted the fix/medium-hardening-2026-05-06 branch May 7, 2026 14:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant