max / makenotwork
| 1 | //! Going the other way: a stored URL back to a key, and a key forward to a |
| 2 | //! public URL. |
| 3 | |
| 4 | /// Extract the S3 key from a CDN or presigned URL. |
| 5 | /// |
| 6 | /// Accepts two URL shapes: |
| 7 | /// - **CDN**: `https://cdn.example.com/{s3_key}`, caller supplies the |
| 8 | /// CDN base; the function strips it verbatim. |
| 9 | /// - **Path-style S3**: `https://{host}/{bucket}/{s3_key}?...`, caller |
| 10 | /// supplies the bucket name; the function strips host + bucket prefix. |
| 11 | /// |
| 12 | /// Returns `None` if neither prefix matches. Query strings (presigned URL |
| 13 | /// signatures) are stripped before returning. |
| 14 | /// |
| 15 | /// **Why explicit prefixes**: the prior implementation used |
| 16 | /// `find("projects/")` as a heuristic, which would silently mis-key any URL |
| 17 | /// whose path happened to contain the literal substring (e.g. a key with a |
| 18 | /// `projects/` suffix inside a user folder). Passing the known CDN base and |
| 19 | /// bucket eliminates the heuristic entirely. |
| 20 | |
| 21 | url: &str, |
| 22 | cdn_base: &str, |
| 23 | bucket: , |
| 24 | s3_endpoint: , |
| 25 | |
| 26 | let no_query = url.split.next?; |
| 27 | |
| 28 | // Try CDN-base prefix first. An empty base matches nothing rather than |
| 29 | // matching everything: `strip_prefix("")` succeeds on any input, so the |
| 30 | // guard is what keeps a caller that passes "" from harvesting a key out of |
| 31 | // an arbitrary host. |
| 32 | if !cdn_base.is_empty |
| 33 | let base = cdn_base.trim_end_matches; |
| 34 | if let Some = no_query.strip_prefix |
| 35 | && let Some = rest.strip_prefix |
| 36 | && !key.is_empty |
| 37 | |
| 38 | return Some; |
| 39 | |
| 40 | |
| 41 | |
| 42 | // Path-style S3: must match the configured `{endpoint}/{bucket}/` exactly. |
| 43 | // Without the endpoint pin, the prior implementation accepted any |
| 44 | // `https://{any-host}/{bucket}/{key}`, so an attacker-controlled URL like |
| 45 | // `https://attacker.example/my-bucket/poisoned` would extract a real-looking |
| 46 | // key and direct downstream code at attacker-chosen storage paths. |
| 47 | if let = |
| 48 | let endpoint = endpoint.trim_end_matches; |
| 49 | let prefix = format!; |
| 50 | if let Some = no_query.strip_prefix |
| 51 | && !key.is_empty |
| 52 | |
| 53 | return Some; |
| 54 | |
| 55 | |
| 56 | |
| 57 | None |
| 58 | |
| 59 | |
| 60 | /// Build a permanent URL for a project image. |
| 61 | /// |
| 62 | /// Permanent is the whole contract: callers persist the result into |
| 63 | /// `projects.cover_image_url`, which is read forever. There is deliberately no |
| 64 | /// presigned fallback — an expiring URL in a durable column is the bug this |
| 65 | /// signature exists to make unrepresentable. `cdn_base` is required config |
| 66 | /// (`Config::cdn_base_url`), so there is nothing to fall back to. |
| 67 | |
| 68 | format! |
| 69 | |
| 70 |