Skip to main content

max / makenotwork

3.6 KB · 109 lines History Blame Raw
1 //! End-to-end coverage for the `/img-proxy` external-image proxy (M-UX1).
2 //!
3 //! These drive the REAL handler with the production SSRF-safe HTTP client wired
4 //! (`link_preview_http`), so they exercise routing -> the login gate -> client
5 //! selection -> `fetch_image` -> `validate_url` refusal, not just the unit
6 //! `validate_url` string check.
7 //!
8 //! Scope note: `validate_url` rejects literal private/reserved targets and bad
9 //! ports/schemes *before* any socket is opened, so these cases are fully
10 //! hermetic (no network, deterministic). The connect-time `SsrfSafeResolver`
11 //! layer, which catches a public *hostname* that resolves to a private address,
12 //! including DNS rebinding, cannot be exercised hermetically: it needs real
13 //! DNS, and any loopback mock server is itself refused as private. That layer
14 //! stays covered by its own resolver logic plus the `validate_url` unit tests.
15
16 use crate::harness::{HarnessOptions, TestHarness};
17 use axum::http::StatusCode;
18
19 async fn http_preview_harness() -> TestHarness {
20 TestHarness::with_options(HarnessOptions {
21 link_preview_http: true,
22 ..Default::default()
23 })
24 .await
25 }
26
27 #[tokio::test]
28 async fn img_proxy_requires_login() {
29 let mut h = http_preview_harness().await;
30 // Auth is checked before any fetch, so even a well-formed target is rejected.
31 let resp = h.client.get("/img-proxy?u=http://example.com/a.png").await;
32 assert_eq!(
33 resp.status,
34 StatusCode::UNAUTHORIZED,
35 "anonymous /img-proxy must be 401, got {}",
36 resp.status
37 );
38 }
39
40 #[tokio::test]
41 async fn img_proxy_refuses_cloud_metadata_ip() {
42 let mut h = http_preview_harness().await;
43 h.login_as("proxyuser_meta").await;
44 let resp = h
45 .client
46 .get("/img-proxy?u=http://169.254.169.254/latest/meta-data/")
47 .await;
48 assert_eq!(
49 resp.status,
50 StatusCode::BAD_GATEWAY,
51 "link-local cloud-metadata IP must be refused (502), got {}",
52 resp.status
53 );
54 }
55
56 #[tokio::test]
57 async fn img_proxy_refuses_loopback() {
58 let mut h = http_preview_harness().await;
59 h.login_as("proxyuser_loop").await;
60 let resp = h.client.get("/img-proxy?u=http://127.0.0.1:1/x.png").await;
61 assert_eq!(
62 resp.status,
63 StatusCode::BAD_GATEWAY,
64 "loopback target must be refused (502), got {}",
65 resp.status
66 );
67 }
68
69 #[tokio::test]
70 async fn img_proxy_refuses_rfc1918_private_ip() {
71 let mut h = http_preview_harness().await;
72 h.login_as("proxyuser_priv").await;
73 let resp = h.client.get("/img-proxy?u=http://10.0.0.5/x.png").await;
74 assert_eq!(
75 resp.status,
76 StatusCode::BAD_GATEWAY,
77 "RFC1918 private IP must be refused (502), got {}",
78 resp.status
79 );
80 }
81
82 #[tokio::test]
83 async fn img_proxy_refuses_non_web_port() {
84 let mut h = http_preview_harness().await;
85 h.login_as("proxyuser_port").await;
86 // Public IP, but a non-web port (Redis), the proxy must not be usable as a
87 // request-forwarder to arbitrary services.
88 let resp = h.client.get("/img-proxy?u=http://1.1.1.1:6379/x").await;
89 assert_eq!(
90 resp.status,
91 StatusCode::BAD_GATEWAY,
92 "non-web port must be refused (502), got {}",
93 resp.status
94 );
95 }
96
97 #[tokio::test]
98 async fn img_proxy_refuses_non_http_scheme() {
99 let mut h = http_preview_harness().await;
100 h.login_as("proxyuser_scheme").await;
101 let resp = h.client.get("/img-proxy?u=ftp://example.com/x.png").await;
102 assert_eq!(
103 resp.status,
104 StatusCode::BAD_GATEWAY,
105 "non-http scheme must be refused (502), got {}",
106 resp.status
107 );
108 }
109