Skip to main content

max / makenotwork

storage: require positive image signature at media confirm; cap build log at SQL layer S1: the content-type sniff only rejected a *positively detected* video, so a video whose container infer cannot name slipped through an image/png declaration and dodged the BigFiles+ video-tier gate. Declared images must now sniff positively as an image (all allowed formats — jpeg/png/webp/gif — are infer-detectable, so no legitimate upload is rejected); declared videos stay lenient so infer-unclassifiable mov/webm still upload. Adds a regression test for the unrecognized-bytes path. M-Sto2: fold a hard octet_length ceiling into append_build_log so the log column cannot grow unbounded regardless of caller — the bound lives in the one function, not a convention each call site must remember. The build_runner soft cap with its truncation marker still fires first. ultra-fuzz Run 5 S1, M-Sto2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-24 01:41 UTC
Commit: 3e429419710917aeca8a01c775a9198531ef66c3
Parent: 2a7aeee
3 files changed, +85 insertions, -11 deletions
@@ -323,12 +323,24 @@ pub async fn fail_stale_running_builds(pool: &PgPool, timeout_secs: i64) -> Resu
323 323 Ok(result.rows_affected())
324 324 }
325 325
326 - /// Append a line to the build log.
326 + /// Hard ceiling on the `log` column, enforced at the SQL layer in
327 + /// [`append_build_log`] so no caller can grow it without bound. Set above the
328 + /// 5 MiB soft cap that `build_runner::append_log_bounded` applies (with its
329 + /// `[log truncated]` marker), so this is a pure backstop: the soft cap fires
330 + /// first for the normal path, and this guarantees the invariant even for a
331 + /// future caller that bypasses the orchestrator.
332 + const HARD_LOG_CAP_BYTES: i64 = 8 * 1024 * 1024;
333 +
334 + /// Append a line to the build log, never letting the column exceed
335 + /// [`HARD_LOG_CAP_BYTES`]. The `octet_length` guard makes unbounded growth
336 + /// unwritable regardless of caller — the bound lives in this one function, not
337 + /// in a convention each call site must remember.
327 338 #[tracing::instrument(skip_all)]
328 339 pub async fn append_build_log(pool: &PgPool, build_id: BuildId, line: &str) -> Result<()> {
329 - sqlx::query("UPDATE ota_builds SET log = log || $2 WHERE id = $1")
340 + sqlx::query("UPDATE ota_builds SET log = log || $2 WHERE id = $1 AND octet_length(log) < $3")
330 341 .bind(build_id)
331 342 .bind(line)
343 + .bind(HARD_LOG_CAP_BYTES)
332 344 .execute(pool)
333 345 .await?;
334 346
@@ -253,17 +253,33 @@ pub(super) async fn media_confirm(
253 253 Err(e) => return Err(AppError::Storage(format!("read media header from S3: {e}"))),
254 254 }
255 255 }
256 - let detected = infer::get(&head).and_then(|kind| match kind.matcher_type() {
257 - infer::MatcherType::Image => Some("image"),
258 - infer::MatcherType::Video => Some("video"),
259 - _ => None,
260 - });
261 - if let Some(real) = detected
262 - && real != media_type
263 - {
256 + let detected = infer::get(&head).map(|kind| kind.matcher_type());
257 + // `media_type` is only ever "image" or "video" (see `classify_media`).
258 + let mismatch = match media_type {
259 + // Every allowed image format (jpeg/png/webp/gif) is positively
260 + // detected by `infer`, so a declared image MUST sniff as an image.
261 + // Requiring a positive image signature — rather than only rejecting
262 + // a *detected* video — closes the bypass where a video `infer`
263 + // cannot name its container is declared `image/png` to dodge the
264 + // BigFiles+ video-tier gate (Run #22 / Run #5 Storage): a video
265 + // never sniffs as Image, so it is rejected here either way.
266 + "image" => detected != Some(infer::MatcherType::Image),
267 + // Declared video: do NOT require a positive video signature —
268 + // `infer` cannot classify every valid container (fragmented mp4,
269 + // some mov/webm), and there is no tier-evasion incentive to declare
270 + // a video as a video. Only reject a still image mislabeled as video.
271 + "video" => detected == Some(infer::MatcherType::Image),
272 + _ => false,
273 + };
274 + if mismatch {
264 275 super::enqueue_s3_orphan(&state.db, &req.s3_key, "media_content_type_mismatch").await;
276 + let sniffed = match detected {
277 + Some(infer::MatcherType::Image) => "image",
278 + Some(infer::MatcherType::Video) => "video",
279 + _ => "an unrecognized format",
280 + };
265 281 return Err(AppError::BadRequest(format!(
266 - "Uploaded file is a {real}, but was declared as a {media_type}."
282 + "Uploaded file does not match the declared {media_type} type (detected: {sniffed})."
267 283 )));
268 284 }
269 285 }
@@ -137,6 +137,52 @@ async fn video_declared_as_image_is_rejected_at_confirm() {
137 137 }
138 138
139 139 // ---------------------------------------------------------------------------
140 + // Bytes `infer` cannot classify, declared as an image, are rejected at confirm
141 + // ---------------------------------------------------------------------------
142 +
143 + #[tokio::test]
144 + async fn unrecognized_bytes_declared_as_image_is_rejected_at_confirm() {
145 + // Run #5 Storage S1: the old sniff only rejected a *positively detected*
146 + // video, so a video whose container `infer` cannot name slipped through an
147 + // `image/png` declaration. Declared images must now sniff positively as an
148 + // image; unrecognized bytes are rejected, closing the bypass.
149 + let mut h = TestHarness::with_storage().await;
150 + setup_basic_creator(&mut h, "unrecimg").await;
151 +
152 + let body = json!({
153 + "file_name": "blob.png",
154 + "content_type": "image/png",
155 + "folder": "clips",
156 + });
157 + let resp = h.client.post_json("/api/media/presign", &body.to_string()).await;
158 + assert!(resp.status.is_success(), "image presign should succeed: {}", resp.text);
159 + let presign: Value = resp.json();
160 + let s3_key = presign["s3_key"].as_str().unwrap().to_string();
161 +
162 + // Bytes `infer` does not recognize as any image or video format.
163 + h.storage.as_ref().unwrap().put(&s3_key, b"not a real image at all".to_vec());
164 +
165 + let body = json!({
166 + "s3_key": s3_key,
167 + "file_name": "blob.png",
168 + "content_type": "image/png",
169 + "folder": "clips",
170 + });
171 + let resp = h.client.post_json("/api/media/confirm", &body.to_string()).await;
172 + assert!(
173 + !resp.status.is_success(),
174 + "unrecognized bytes declared as an image must be rejected, got: {} {}",
175 + resp.status, resp.text
176 + );
177 +
178 + let list: Value = h.client.get("/api/media?folder=clips").await.json();
179 + assert!(
180 + list["files"].as_array().map(|f| f.is_empty()).unwrap_or(true),
181 + "rejected upload must not appear in the media library"
182 + );
183 + }
184 +
185 + // ---------------------------------------------------------------------------
140 186 // Image upload on Basic tier succeeds (images bypass tier check like covers)
141 187 // ---------------------------------------------------------------------------
142 188