Skip to content

Commit ca5e7da

Browse files
committed
fix: configurable trusted IP header (do-connecting-ip), reject http:// URLs
- Replace TRUST_PROXY bool with TRUSTED_IP_HEADER string (set to do-connecting-ip for DO App Platform, empty to disable) - Only accept https:// URLs (http:// causes mixed-content in the editor)
1 parent 5c1fd97 commit ca5e7da

4 files changed

Lines changed: 18 additions & 18 deletions

File tree

agents/.do/app.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ services:
3232
- key: DEFAULT_EDITOR_HOST
3333
scope: RUN_TIME
3434
value: ai.simplepdf.com
35-
- key: TRUST_PROXY
35+
- key: TRUSTED_IP_HEADER
3636
scope: RUN_TIME
37-
value: "true"
37+
value: do-connecting-ip
3838
- key: RATE_LIMIT_PER_MIN
3939
scope: RUN_TIME
4040
value: "30"

agents/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ Once the user opens the editor link, they can:
151151
- Maximum PDF size: 50 MB (file uploads only)
152152
- Uploaded files expire after 24 hours
153153
- Rate limit: 30 requests per minute per IP
154-
- URL must start with http:// or https://
154+
- URL must start with https://
155155
- companyIdentifier must be a valid SimplePDF portal identifier
156156

157157
## Legal

agents/src/config.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ pub struct Config {
44
pub s3_region: String,
55
pub default_editor_host: String,
66
pub rate_limit_per_minute: u32,
7-
pub trust_proxy: bool,
7+
pub trusted_ip_header: String,
88
}
99

1010
impl Config {
@@ -17,7 +17,7 @@ impl Config {
1717
rate_limit_per_minute: env("RATE_LIMIT_PER_MIN")
1818
.parse()
1919
.expect("RATE_LIMIT_PER_MIN must be a number"),
20-
trust_proxy: env("TRUST_PROXY") == "true",
20+
trusted_ip_header: env("TRUSTED_IP_HEADER"),
2121
}
2222
}
2323

agents/src/routes.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -89,15 +89,13 @@ async fn handle_get(
8989
Some(url) => url,
9090
};
9191

92-
let ip = client_ip(&request, addr.ip(), state.config.trust_proxy);
92+
let ip = client_ip(&request, addr.ip(), &state.config.trusted_ip_header);
9393
if !state.rate_limiter.check(ip) {
9494
return Err(AppError::RateLimited);
9595
}
9696

9797
if !is_valid_url(&pdf_url) {
98-
return Err(AppError::BadRequest(
99-
"url must start with http:// or https://".into(),
100-
));
98+
return Err(AppError::BadRequest("url must start with https://".into()));
10199
}
102100

103101
let company_identifier = validate_company_identifier(query.company_identifier.as_deref())?;
@@ -117,7 +115,7 @@ async fn handle_post(
117115
Query(query): Query<PostQuery>,
118116
request: axum::extract::Request,
119117
) -> Result<Json<AgentResponse>, AppError> {
120-
let ip = client_ip(&request, addr.ip(), state.config.trust_proxy);
118+
let ip = client_ip(&request, addr.ip(), &state.config.trusted_ip_header);
121119
if !state.rate_limiter.check(ip) {
122120
return Err(AppError::RateLimited);
123121
}
@@ -161,9 +159,7 @@ async fn handle_post(
161159
})?;
162160

163161
if !is_valid_url(&input.url) {
164-
return Err(AppError::BadRequest(
165-
"url must start with http:// or https://".into(),
166-
));
162+
return Err(AppError::BadRequest("url must start with https://".into()));
167163
}
168164

169165
let company_identifier = validate_company_identifier(
@@ -211,16 +207,20 @@ async fn extract_multipart(mut multipart: Multipart) -> Result<MultipartUpload,
211207
))
212208
}
213209

214-
fn client_ip(request: &axum::extract::Request, fallback: IpAddr, trust_proxy: bool) -> IpAddr {
215-
if !trust_proxy {
210+
fn client_ip(
211+
request: &axum::extract::Request,
212+
fallback: IpAddr,
213+
trusted_ip_header: &str,
214+
) -> IpAddr {
215+
if trusted_ip_header.is_empty() {
216216
return fallback;
217217
}
218218

219219
request
220220
.headers()
221-
.get("x-forwarded-for")
221+
.get(trusted_ip_header)
222222
.and_then(|v| v.to_str().ok())
223-
.and_then(|v| v.rsplit(',').next())
223+
.and_then(|v| v.split(',').next())
224224
.and_then(|v| v.trim().parse::<IpAddr>().ok())
225225
.unwrap_or(fallback)
226226
}
@@ -266,7 +266,7 @@ fn is_valid_subdomain(identifier: &str) -> bool {
266266
}
267267

268268
fn is_valid_url(url: &str) -> bool {
269-
url.starts_with("https://") || url.starts_with("http://")
269+
url.starts_with("https://")
270270
}
271271

272272
fn validate_company_identifier(identifier: Option<&str>) -> Result<Option<&str>, AppError> {

0 commit comments

Comments
 (0)