mod common;
use common::{
png_bytes, send_transform_request, spawn_fixture_server, spawn_server, split_response, temp_dir,
};
use std::fs;
use truss::ServerConfig;
#[test]
fn ssrf_redirect_to_metadata_endpoint_is_blocked() {
let storage_root = temp_dir("ssrf-redirect-metadata");
let (url, fixture) = spawn_fixture_server(vec![(
"302 Found".to_string(),
vec![(
"Location".to_string(),
"http://169.254.169.254/latest/meta-data".to_string(),
)],
Vec::new(),
)]);
let (addr, handle) = spawn_server(
ServerConfig::new(storage_root, Some("secret".to_string())).with_insecure_url_sources(true),
);
let body =
format!(r#"{{"source":{{"kind":"url","url":"{url}"}},"options":{{"format":"jpeg"}}}}"#);
let response = send_transform_request(addr, &body, Some("secret"));
handle
.join()
.expect("join server thread")
.expect("serve one request");
fixture.join().expect("join fixture server");
let (header, content_type, body) = split_response(&response);
let body = String::from_utf8(body).expect("utf8 response body");
assert!(
header.starts_with("HTTP/1.1 403"),
"redirect to metadata should be blocked, got: {header}"
);
assert_eq!(content_type, "application/problem+json");
assert!(
body.contains("cloud metadata"),
"error should mention cloud metadata, got: {body}"
);
}
#[test]
fn ssrf_ftp_scheme_rejected() {
let storage_root = temp_dir("ssrf-ftp");
let (addr, handle) = spawn_server(
ServerConfig::new(storage_root, Some("secret".to_string())).with_insecure_url_sources(true),
);
let response = send_transform_request(
addr,
r#"{"source":{"kind":"url","url":"ftp://evil.com/image.png"},"options":{"format":"jpeg"}}"#,
Some("secret"),
);
handle
.join()
.expect("join server thread")
.expect("serve one request");
let (header, _, body) = split_response(&response);
let body = String::from_utf8(body).expect("utf8");
assert!(
header.starts_with("HTTP/1.1 400"),
"ftp scheme should be rejected, got: {header}"
);
assert!(body.contains("http"));
}
#[test]
fn ssrf_file_scheme_rejected() {
let storage_root = temp_dir("ssrf-file");
let (addr, handle) = spawn_server(
ServerConfig::new(storage_root, Some("secret".to_string())).with_insecure_url_sources(true),
);
let response = send_transform_request(
addr,
r#"{"source":{"kind":"url","url":"file:///etc/passwd"},"options":{"format":"jpeg"}}"#,
Some("secret"),
);
handle
.join()
.expect("join server thread")
.expect("serve one request");
let (header, _, _) = split_response(&response);
assert!(
header.starts_with("HTTP/1.1 400"),
"file scheme should be rejected, got: {header}"
);
}
#[test]
fn ssrf_data_scheme_rejected() {
let storage_root = temp_dir("ssrf-data");
let (addr, handle) = spawn_server(
ServerConfig::new(storage_root, Some("secret".to_string())).with_insecure_url_sources(true),
);
let response = send_transform_request(
addr,
r#"{"source":{"kind":"url","url":"data:image/png;base64,iVBOR"},"options":{"format":"jpeg"}}"#,
Some("secret"),
);
handle
.join()
.expect("join server thread")
.expect("serve one request");
let (header, _, _) = split_response(&response);
assert!(
header.starts_with("HTTP/1.1 400"),
"data scheme should be rejected, got: {header}"
);
}
#[test]
fn ssrf_url_with_userinfo_rejected() {
let storage_root = temp_dir("ssrf-userinfo");
let (addr, handle) = spawn_server(
ServerConfig::new(storage_root, Some("secret".to_string())).with_insecure_url_sources(true),
);
let response = send_transform_request(
addr,
r#"{"source":{"kind":"url","url":"http://admin:pass@example.com/image.png"},"options":{"format":"jpeg"}}"#,
Some("secret"),
);
handle
.join()
.expect("join server thread")
.expect("serve one request");
let (header, _, body) = split_response(&response);
let body = String::from_utf8(body).expect("utf8");
assert!(
header.starts_with("HTTP/1.1 400"),
"URL with userinfo should be rejected, got: {header}"
);
assert!(body.contains("user"));
}
#[test]
fn ssrf_private_ip_ranges_blocked_in_strict_mode() {
let _storage_root = temp_dir("ssrf-private-strict");
let blocked_urls = [
("http://10.0.0.1/img.png", "10.0.0.0/8 private"),
("http://172.16.0.1/img.png", "172.16.0.0/12 private"),
("http://192.168.1.1/img.png", "192.168.0.0/16 private"),
];
for (url, description) in blocked_urls {
let storage = temp_dir(&format!("ssrf-priv-{}", description.replace('/', "-")));
let (addr, handle) = spawn_server(ServerConfig::new(storage, Some("secret".to_string())));
let body =
format!(r#"{{"source":{{"kind":"url","url":"{url}"}},"options":{{"format":"jpeg"}}}}"#);
let response = send_transform_request(addr, &body, Some("secret"));
handle
.join()
.expect("join server thread")
.expect("serve one request");
let (header, _, _) = split_response(&response);
assert!(
header.starts_with("HTTP/1.1 403"),
"{description} should be blocked in strict mode, got: {header}"
);
}
}
#[test]
fn ssrf_non_standard_port_blocked_in_strict_mode() {
let storage_root = temp_dir("ssrf-port-strict");
let (addr, handle) = spawn_server(ServerConfig::new(storage_root, Some("secret".to_string())));
let response = send_transform_request(
addr,
r#"{"source":{"kind":"url","url":"http://example.com:8080/image.png"},"options":{"format":"jpeg"}}"#,
Some("secret"),
);
handle
.join()
.expect("join server thread")
.expect("serve one request");
let (header, _, body) = split_response(&response);
let body = String::from_utf8(body).expect("utf8");
assert!(
header.starts_with("HTTP/1.1 403"),
"non-standard port should be blocked, got: {header}"
);
assert!(body.contains("port"));
}
#[test]
fn path_traversal_via_transform_request_is_rejected() {
let storage_root = temp_dir("path-traversal-e2e");
fs::write(storage_root.join("legit.png"), png_bytes()).expect("write legit image");
let (addr, handle) = spawn_server(ServerConfig::new(storage_root, Some("secret".to_string())));
let response = send_transform_request(
addr,
r#"{"source":{"kind":"path","path":"../../etc/passwd"},"options":{"format":"jpeg"}}"#,
Some("secret"),
);
handle
.join()
.expect("join server thread")
.expect("serve one request");
let (header, _, _) = split_response(&response);
assert!(
header.starts_with("HTTP/1.1 400"),
"path traversal should be rejected, got: {header}"
);
}
#[test]
fn path_traversal_with_dotdot_in_middle_is_rejected() {
let storage_root = temp_dir("path-traversal-mid");
fs::create_dir_all(storage_root.join("sub")).expect("create subdir");
fs::write(storage_root.join("sub/image.png"), png_bytes()).expect("write image");
let (addr, handle) = spawn_server(ServerConfig::new(storage_root, Some("secret".to_string())));
let response = send_transform_request(
addr,
r#"{"source":{"kind":"path","path":"/sub/../../../etc/passwd"},"options":{"format":"jpeg"}}"#,
Some("secret"),
);
handle
.join()
.expect("join server thread")
.expect("serve one request");
let (header, _, _) = split_response(&response);
assert!(
header.starts_with("HTTP/1.1 400"),
"mid-path traversal should be rejected, got: {header}"
);
}
#[test]
fn dotgit_access_does_not_leak_raw_file_content() {
let storage_root = temp_dir("path-traversal-git");
fs::create_dir_all(storage_root.join(".git/logs")).expect("create .git dir");
let secret_content = b"ref: refs/heads/main\n";
fs::write(storage_root.join(".git/logs/HEAD"), secret_content).expect("write git log");
let (addr, handle) = spawn_server(ServerConfig::new(storage_root, Some("secret".to_string())));
let response = send_transform_request(
addr,
r#"{"source":{"kind":"path","path":"/.git/logs/HEAD"},"options":{"format":"jpeg"}}"#,
Some("secret"),
);
handle
.join()
.expect("join server thread")
.expect("serve one request");
let (header, content_type, body) = split_response(&response);
assert!(
header.starts_with("HTTP/1.1 415"),
".git file should be rejected as non-image (415), got: {header}"
);
assert_eq!(content_type, "application/problem+json");
assert!(
!body
.windows(secret_content.len())
.any(|w| w == secret_content),
"response must not leak raw .git file content"
);
}
#[test]
fn remote_upstream_4xx_returns_502() {
let storage_root = temp_dir("remote-4xx");
let (url, fixture) = spawn_fixture_server(vec![(
"404 Not Found".to_string(),
vec![("Content-Type".to_string(), "text/plain".to_string())],
b"not found".to_vec(),
)]);
let (addr, handle) = spawn_server(
ServerConfig::new(storage_root, Some("secret".to_string())).with_insecure_url_sources(true),
);
let body =
format!(r#"{{"source":{{"kind":"url","url":"{url}"}},"options":{{"format":"jpeg"}}}}"#);
let response = send_transform_request(addr, &body, Some("secret"));
handle
.join()
.expect("join server thread")
.expect("serve one request");
fixture.join().expect("join fixture server");
let (header, content_type, body) = split_response(&response);
let body = String::from_utf8(body).expect("utf8");
assert!(
header.starts_with("HTTP/1.1 502"),
"upstream 404 should map to 502, got: {header}"
);
assert_eq!(content_type, "application/problem+json");
assert!(body.contains("upstream HTTP 404"));
}
#[test]
fn remote_upstream_5xx_returns_502() {
let storage_root = temp_dir("remote-5xx");
let (url, fixture) = spawn_fixture_server(vec![(
"500 Internal Server Error".to_string(),
vec![("Content-Type".to_string(), "text/plain".to_string())],
b"server error".to_vec(),
)]);
let (addr, handle) = spawn_server(
ServerConfig::new(storage_root, Some("secret".to_string())).with_insecure_url_sources(true),
);
let body =
format!(r#"{{"source":{{"kind":"url","url":"{url}"}},"options":{{"format":"jpeg"}}}}"#);
let response = send_transform_request(addr, &body, Some("secret"));
handle
.join()
.expect("join server thread")
.expect("serve one request");
fixture.join().expect("join fixture server");
let (header, _, body) = split_response(&response);
let body = String::from_utf8(body).expect("utf8");
assert!(
header.starts_with("HTTP/1.1 502"),
"upstream 500 should map to 502, got: {header}"
);
assert!(body.contains("upstream HTTP 500"));
}
#[test]
fn remote_upstream_403_returns_502() {
let storage_root = temp_dir("remote-403");
let (url, fixture) = spawn_fixture_server(vec![(
"403 Forbidden".to_string(),
vec![("Content-Type".to_string(), "text/plain".to_string())],
b"forbidden".to_vec(),
)]);
let (addr, handle) = spawn_server(
ServerConfig::new(storage_root, Some("secret".to_string())).with_insecure_url_sources(true),
);
let body =
format!(r#"{{"source":{{"kind":"url","url":"{url}"}},"options":{{"format":"jpeg"}}}}"#);
let response = send_transform_request(addr, &body, Some("secret"));
handle
.join()
.expect("join server thread")
.expect("serve one request");
fixture.join().expect("join fixture server");
let (header, _, body) = split_response(&response);
let body = String::from_utf8(body).expect("utf8");
assert!(
header.starts_with("HTTP/1.1 502"),
"upstream 403 should map to 502, got: {header}"
);
assert!(body.contains("upstream HTTP 403"));
}