Skip to content

fix(media): recognize safe iCalendar attachments - #6385

Open
liowald wants to merge 14 commits into
block:mainfrom
liowald:fix/safe-calendar-attachments
Open

fix(media): recognize safe iCalendar attachments#6385
liowald wants to merge 14 commits into
block:mainfrom
liowald:fix/safe-calendar-attachments

Conversation

@liowald

@liowald liowald commented Aug 20, 2026

Copy link
Copy Markdown

Problem

On current origin/main (e5d1dfef7bf24ad527c9c8c1785b613abad574f7), .ics has no infer magic-byte signature. A valid calendar becomes application/octet-stream, and buzz upload file --file Planning.ics is rejected before network I/O as unsupported.

Buzz deliberately supports generic non-media attachments through a deny-list plus inert forced downloads (#5569). This PR keeps PDF, ZIP, Office, text, HTML, archives, audio, images, and video unchanged.

Smallest approach

  • Treat exact .ics / text/calendar metadata as untrusted hints on standard Blossom /upload.
  • Canonically recognize valid calendar bytes without hints so one hash cannot receive conflicting sidecars.
  • Require at most 10 MiB, UTF-8, no NUL, valid unfolded content lines, one complete VCALENDAR envelope, and balanced components.
  • Preserve a sanitized .ics filename from the relay-authoritative returned MIME, including generic or extensionless input names.
  • Keep /media/upload media-only and never retry an explicitly signaled calendar there.
  • Reuse existing attachment/FileCard rendering. There is no visual change, so screenshots are not applicable.

Existing boundaries remain authoritative: hash-bound Blossom auth, tenant sidecars, byte-identical storage, Content-Disposition: attachment, nosniff, and CSP default-src none.

Related work

The closest overlapping generic CLI contribution is #4880. Also related: #3641, #2251, #2813, closed duplicate #5585, and issues #2963, #3453, and #5675. This PR does not duplicate their broader generic attachment or preview work.

Verification

  • Baseline/sabotage: current main rejects valid .ics; old client logic fails the new descriptor-authoritative regression by emitting Planning.txt instead of Planning.ics.
  • cargo test -p buzz-media — 123 passed.
  • cargo test -p buzz-cli — 353 passed.
  • Focused relay media/imeta suites and affected Rust clippy — passed.
  • Mobile upload suite — 38 passed; Flutter analyze and Dart format checks passed.
  • pnpm run check, Rust formatting, and diff checks — passed (only unrelated pre-existing desktop Biome warnings/infos).
  • Calendar E2E covers standard upload/download, legacy-route rejection, disguised JPEG/MP4 rejection, and wrapped-HTML rejection; generic HTML/PDF/octet-stream/PNG/MP4 regressions pass.
  • Final authenticated release relay + buzz messages send --file round-trip produced event 4450fba09e617b314739775ed3f5cefdcbc72dae659b6192231b01e7fc7c890a, m text/calendar, filename calendar-roundtrip.ics, and byte-identical SHA-256 030c68c5a7f509b07117e617d646e6683de135eee09d9996e721ea20dc6fb4d5.

just ci cannot run the desktop Tauri Rust lane on this host because GTK/GLib/GIO development packages (glib-2.0.pc, gobject-2.0.pc, gdk-3.0.pc) are absent. Tauri tests are included for supported CI hosts; all runnable focused gates above pass.

Non-goals

No new dependency, endpoint, schema, preview UI, renderer, or generic attachment-policy rewrite. This PR is ready for Block maintainer review.

Reconciliation update

  • Integrated current block/buzz:main at 24ec6a468ec9d0d425ee58fbfc4d416412c446ad; GitHub now reports the PR mergeable.
  • Addressed the three reviewed calendar-path defects: generic non-media descriptors render as named file downloads, RFC-folded bytes are unfolded before UTF-8 validation, and relay validation requires a safe .ics calendar filename.
  • Final exact head: 389cefa6465525232f0ea20e5b41afc3bdb2a18d (signed off).
  • Final local gates: pnpm run check, Rust formatting, diff checks, and the three focused regression tests passed. Two independent factory reviewers approved the identical tree.
  • GitHub DCO passes; Block maintainer review remains required.

Signed-off-by: liowald <l.diwald@gmail.com>
Signed-off-by: liowald <l.diwald@gmail.com>
Signed-off-by: liowald <l.diwald@gmail.com>
Signed-off-by: liowald <l.diwald@gmail.com>
Signed-off-by: liowald <l.diwald@gmail.com>
@liowald liowald changed the title fix(media): support safe calendar attachments fix(media): recognize safe iCalendar attachments Aug 20, 2026
Signed-off-by: liowald <l.diwald@gmail.com>
Signed-off-by: liowald <l.diwald@gmail.com>
Signed-off-by: liowald <l.diwald@gmail.com>
Signed-off-by: liowald <l.diwald@gmail.com>
@liowald
liowald marked this pull request as ready for review August 20, 2026 16:47
@liowald
liowald requested a review from a team as a code owner August 20, 2026 16:47

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ae6a770e14

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/buzz-cli/src/commands/messages.rs
Comment thread crates/buzz-media/src/validation.rs Outdated
Comment thread crates/buzz-relay/src/handlers/imeta.rs Outdated
Signed-off-by: liowald <l.diwald@gmail.com>
Signed-off-by: liowald <l.diwald@gmail.com>
@liowald
liowald marked this pull request as draft August 21, 2026 08:45

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 389cefa646

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/buzz-media/src/validation.rs Outdated
Comment on lines +259 to +260
fn signals_calendar_content(bytes: &[u8]) -> bool {
let unfolded = unfold_calendar_bytes(bytes);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid copying every generic attachment during calendar detection

When members concurrently upload large non-calendar attachments, signals_calendar_content copies each entire payload into a new Vec before determining that it is not a calendar. With the default 100 MiB generic-file limit and eight concurrent uploads, this adds up to 800 MiB on top of the already buffered request bodies and can cause relay OOMs. Detect the ASCII calendar envelope without cloning the whole body, reserving unfolding for payloads that actually require calendar validation.

Useful? React with 👍 / 👎.

Comment thread crates/buzz-media/src/validation.rs Outdated
Comment on lines +248 to +253
if declared_mime != Some("text/calendar") || extension != Some("ics") {
return Err(MediaError::DisallowedContentType(
declared_mime
.unwrap_or("application/octet-stream")
.to_string(),
));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Accept calendar uploads without the custom extension header

When a standard Blossom client uploads valid calendar bytes with Content-Type: text/calendar but no Buzz-specific X-Buzz-File-Extension, this branch rejects the request even though the same bytes succeed when sent as application/octet-stream via the content-based detector. Since validation already establishes that the body is a safe calendar, requiring both hints unnecessarily prevents interoperable clients from using the advertised Blossom-compatible upload endpoint.

Useful? React with 👍 / 👎.

@liowald
liowald marked this pull request as ready for review August 21, 2026 08:53
@liowald

liowald commented Aug 21, 2026

Copy link
Copy Markdown
Author

Resolved the conflict with current main and addressed the three automated review findings. The updated branch is mergeable, DCO is passing, and the PR is ready for re-review.

@block/buzz-oss-team

Signed-off-by: liowald <l.diwald@gmail.com>

# Conflicts:
#	crates/buzz-cli/src/commands/messages.rs
#	desktop/src-tauri/src/commands/media.rs
Signed-off-by: liowald <l.diwald@gmail.com>
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

🔐 Codex Security Review

Status: review required for the current range.

The current range is 1c8321cd08feb597f8bcff5195c21148fb3e98ed...b8383c019778e843ca4953532acd70349de5e8b2.
A new review must complete for this exact range. When manual authorization
is required, a Block organization member must comment exactly
@buzz-security-review b8383c019778e843ca4953532acd70349de5e8b2 to authorize a new review.
Any previous review applies only to its recorded range.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-02T14:05:21.420514Z b8383c0 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9fc248629d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +305 to +306
for raw_line in text.split('\n') {
let line = raw_line.strip_suffix('\r').unwrap_or(raw_line);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject bare carriage returns in calendar lines

When an upload mixes CRLF separators with bare \r characters, splitting only on \n leaves the bare carriage returns embedded in a property value, so structural content lines can bypass the component checks. For example, SUMMARY:x\rBEGIN:VCALENDAR\rEND:VCALENDAR is treated as one harmless SUMMARY line and the upload is accepted, although calendar consumers that recognize bare-CR line endings see the hidden nested envelope. Reject bare carriage returns or tokenize all accepted line endings before validating component balance.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in b8383c0. Calendar validation now rejects every bare CR before unfolding/parsing, with regressions for both hidden-component and bare-CR-separator inputs.

Comment on lines +187 to +191
// Canonicalize valid iCalendar bytes even when an older/generic client did
// not send hints. Sidecars are keyed only by the byte hash, so the same
// bytes must never alternate between `.bin` and `.ics` classifications.
if signals_calendar_content(bytes) {
return validate_calendar_content(bytes, config);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve existing URLs when reclassifying calendar hashes

When these bytes were uploaded before this change, the generic path stored them as <hash>.bin with a hash-keyed sidecar whose authoritative extension is bin; mobile and desktop already allowed such generic uploads. Reuploading the same calendar after an upgrade now returns ics, causing the buffered pipeline to write <hash>.ics and overwrite that shared sidecar. The serve path rejects an explicit extension that differs from the sidecar, so every immutable message still referencing <hash>.bin starts returning 404. Preserve an existing classification or provide an alias/migration before changing the authoritative sidecar.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in b8383c0. Existing hash sidecars are authoritative for buffered and video re-uploads, and first-writer classification claims plus conditional sidecar publication prevent updated writers from replacing immutable URLs. Live-MinIO regressions cover the pre-existing generic URL and moderation-record classification.

Comment thread crates/buzz-media/src/validation.rs Outdated
Comment on lines +304 to +305
let mut lines: Vec<String> = Vec::new();
for raw_line in text.split('\n') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Bound the number of calendar content lines

The 10 MiB byte limit does not bound this derived representation: a valid envelope containing millions of short X:\n properties makes this loop allocate a separate String for every line plus a 24-byte vector entry and allocator metadata. With the relay's default eight concurrent uploads, small authenticated requests can therefore amplify into roughly gigabytes of heap and exhaust the process. Parse without per-line ownership or impose a content-line/component limit before populating this vector.

AGENTS.md reference: AGENTS.md:L194-L202

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in b8383c0. Parsing keeps borrowed line slices, enforces a 100,000 physical/content-line cap before structural work can amplify, and includes adversarial non-empty and blank-line regressions.

Signed-off-by: liowald <l.diwald@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b8383c0197

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

uploaded_at,
},
)
.await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Make upload-record correction durable

When upload records are enabled during a rolling deployment, a legacy writer can publish a different sidecar after this request has already persisted its record; if this corrective PUT then fails transiently, the original record remains permanently committed with the wrong MIME and extension. Retrying the upload creates another record but never repairs the incorrect append-only moderation/legal record, so determine the canonical facts before the initial write or durably retry the correction.

AGENTS.md reference: AGENTS.md:L204-L209

Useful? React with 👍 / 👎.

Comment on lines +174 to +175
if !filename_value.is_empty() && !filename_value.to_ascii_lowercase().ends_with(".ics")
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require a filename on calendar imeta tags

When a client submits a text/calendar imeta tag without filename, this conditional accepts it, leaving file-card consumers to display the hash-based URL tail instead of the calendar's user-facing name. Fresh evidence in this exact commit is the new !filename_value.is_empty() guard and regression test accepting omission, whereas the review thread records that the approved contract requires one safe .ics filename; reject an empty filename as well as a non-.ics one.

Useful? React with 👍 / 👎.

Comment on lines +419 to +420
|| (components.is_empty() && !value.eq_ignore_ascii_case("VCALENDAR"))
|| (!components.is_empty() && value.eq_ignore_ascii_case("VCALENDAR"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject a second top-level VCALENDAR envelope

When an upload concatenates two complete calendars, closing the first empties components, so this condition permits another BEGIN:VCALENDAR; the final checks then see a balanced stack and the second closing line and accept the whole payload. This violates the validator's one-envelope contract and creates parser-dependent contents where consumers may process only the first calendar or both, so track that the outer calendar has already been opened or closed and reject any subsequent top-level envelope.

Useful? React with 👍 / 👎.

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