Skip to content

Commit de97533

Browse files
Use valid image proxy content type fallback
1 parent 54a0f3d commit de97533

2 files changed

Lines changed: 34 additions & 14 deletions

File tree

crates/trusted-server-core/src/proxy.rs

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ use crate::streaming_processor::{Compression, PipelineConfig, StreamProcessor, S
1818
/// Chunk size used for streaming content through the rewrite pipeline.
1919
const STREAMING_CHUNK_SIZE: usize = 8192;
2020

21+
/// Fallback `Content-Type` for image-like responses missing an origin type.
22+
const IMAGE_FALLBACK_CONTENT_TYPE: &str = "application/octet-stream";
23+
2124
#[derive(Deserialize)]
2225
struct ProxySignReq {
2326
url: String,
@@ -256,7 +259,7 @@ fn finalize_proxied_response(
256259
);
257260
}
258261

259-
// Image handling: set generic content-type if missing and log pixel heuristics
262+
// Image handling: set a valid fallback content type if missing and log pixel heuristics
260263
let req_accept_images = req
261264
.get_header(HEADER_ACCEPT)
262265
.and_then(|h| h.to_str().ok())
@@ -265,7 +268,7 @@ fn finalize_proxied_response(
265268

266269
if ct.starts_with("image/") || req_accept_images {
267270
if beresp.get_header(header::CONTENT_TYPE).is_none() {
268-
beresp.set_header(header::CONTENT_TYPE, "image/*");
271+
beresp.set_header(header::CONTENT_TYPE, IMAGE_FALLBACK_CONTENT_TYPE);
269272
}
270273

271274
// Heuristics to log likely tracking pixels without altering response
@@ -344,7 +347,7 @@ fn finalize_proxied_response_streaming(
344347

345348
if ct.starts_with("image/") || req_accept_images {
346349
if beresp.get_header(header::CONTENT_TYPE).is_none() {
347-
beresp.set_header(header::CONTENT_TYPE, "image/*");
350+
beresp.set_header(header::CONTENT_TYPE, IMAGE_FALLBACK_CONTENT_TYPE);
348351
}
349352

350353
let mut is_pixel = false;
@@ -685,9 +688,9 @@ async fn proxy_with_redirects(
685688
/// - Proxies the decoded URL via a dynamic backend derived from scheme/host/port.
686689
/// - If the response `Content-Type` contains `text/html`, rewrites the HTML creative
687690
/// (img/srcset/iframe to first-party) before returning `text/html; charset=utf-8`.
688-
/// - If the response is an image or the request `Accept` indicates images, ensures a
689-
/// generic `image/*` content type if origin omitted it, and logs likely 1×1 pixels
690-
/// using simple size/URL heuristics. No special response (still proxied).
691+
/// - If the response is an image or the request `Accept` indicates images, ensures an
692+
/// `application/octet-stream` content type if origin omitted it, and logs likely 1×1
693+
/// pixels using simple size/URL heuristics. No special response (still proxied).
691694
///
692695
/// # Errors
693696
///
@@ -1168,7 +1171,7 @@ mod tests {
11681171
copy_proxy_forward_headers, handle_first_party_click, handle_first_party_proxy,
11691172
handle_first_party_proxy_rebuild, handle_first_party_proxy_sign, is_host_allowed,
11701173
rebuild_response_with_body, reconstruct_and_validate_signed_target, redirect_is_permitted,
1171-
ProxyRequestConfig, SUPPORTED_ENCODINGS,
1174+
ProxyRequestConfig, IMAGE_FALLBACK_CONTENT_TYPE, SUPPORTED_ENCODINGS,
11721175
};
11731176
use crate::error::{IntoHttpResponse, TrustedServerError};
11741177
use crate::test_support::tests::create_test_settings;
@@ -1625,8 +1628,9 @@ mod tests {
16251628

16261629
// --- Finalization path tests (no network) ---
16271630

1628-
// Access the finalize function within the crate for testing
1631+
// Access the finalize helpers within the crate for testing
16291632
use super::finalize_proxied_response as finalize;
1633+
use super::finalize_proxied_response_streaming as finalize_streaming;
16301634

16311635
#[test]
16321636
fn html_response_is_rewritten_and_content_type_set() {
@@ -1728,20 +1732,36 @@ mod tests {
17281732
}
17291733

17301734
#[test]
1731-
fn image_accept_sets_generic_content_type_when_missing() {
1735+
fn image_accept_sets_fallback_content_type_when_missing() {
17321736
let settings = create_test_settings();
17331737
let beresp = Response::from_status(StatusCode::OK).with_body("PNG");
17341738
let mut req = Request::new(Method::GET, "https://edge.example/first-party/proxy");
17351739
req.set_header(HEADER_ACCEPT, "image/*");
17361740
let out = finalize(&settings, &req, "https://cdn.example/pixel.gif", beresp)
17371741
.expect("finalize should succeed");
1738-
// Since CT was missing and Accept indicates image, it should set generic image/*
1742+
// Since CT was missing and Accept indicates image, it should set a valid fallback.
17391743
let ct = out
17401744
.get_header(header::CONTENT_TYPE)
17411745
.expect("Content-Type header should be present")
17421746
.to_str()
17431747
.expect("Content-Type should be valid UTF-8");
1744-
assert_eq!(ct, "image/*");
1748+
assert_eq!(ct, IMAGE_FALLBACK_CONTENT_TYPE);
1749+
}
1750+
1751+
#[test]
1752+
fn streaming_image_accept_sets_fallback_content_type_when_missing() {
1753+
let beresp = Response::from_status(StatusCode::OK).with_body("GIF");
1754+
let mut req = Request::new(Method::GET, "https://edge.example/first-party/proxy");
1755+
req.set_header(HEADER_ACCEPT, "image/*");
1756+
1757+
let out = finalize_streaming(&req, "https://cdn.example/pixel.gif", beresp);
1758+
let ct = out
1759+
.get_header(header::CONTENT_TYPE)
1760+
.expect("Content-Type header should be present")
1761+
.to_str()
1762+
.expect("Content-Type should be valid UTF-8");
1763+
1764+
assert_eq!(ct, IMAGE_FALLBACK_CONTENT_TYPE);
17451765
}
17461766

17471767
#[test]

docs/guide/first-party-proxy.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ GET /first-party/proxy?tsurl=https://example.com/ad.html&tstoken=signature
5757
4. **Processes** response based on content type:
5858
- **HTML** (`text/html`) - Rewrites all URLs, returns `text/html`
5959
- **CSS** (`text/css`) - Rewrites `url()` values, returns `text/css`
60-
- **Images** - Detects pixels, sets `image/*` if missing
60+
- **Images** - Detects pixels, sets `application/octet-stream` if missing
6161
- **Other** - Passthrough without modification
6262

6363
**Example**:
@@ -291,12 +291,12 @@ For the detailed signing algorithm, validation steps, and security notes, see [P
291291

292292
**Triggers**:
293293

294-
- Response `Content-Type: image/*`, OR
294+
- Response `Content-Type` starts with `image/`, OR
295295
- Request `Accept` header contains `image/`
296296

297297
**Process**:
298298

299-
1. Set `Content-Type: image/*` if missing
299+
1. Set `Content-Type: application/octet-stream` if missing
300300
2. Detect likely pixels with heuristics:
301301
- `Content-Length` ≤ 256 bytes
302302
- URL contains `/pixel`, `/p.gif`, `/1x1`, `/track`

0 commit comments

Comments
 (0)