|
17 | 17 | import org.springframework.http.ResponseEntity; |
18 | 18 | import org.springframework.web.bind.annotation.*; |
19 | 19 |
|
| 20 | +import java.net.InetAddress; |
| 21 | +import java.net.URI; |
| 22 | +import java.net.URISyntaxException; |
20 | 23 | import java.util.HashMap; |
21 | 24 | import java.util.List; |
22 | 25 | import java.util.Map; |
@@ -62,6 +65,12 @@ public ResponseEntity<Map<String, Object>> retrieveDocument( |
62 | 65 | .body(Map.of("error", "sourceUrl is required")); |
63 | 66 | } |
64 | 67 |
|
| 68 | + if (!isAllowedSourceUrl(sourceUrl)) { |
| 69 | + log.warn("Rejected document retrieval request to disallowed URL: {}", sourceUrl); |
| 70 | + return ResponseEntity.badRequest() |
| 71 | + .body(Map.of("error", "Invalid or disallowed sourceUrl")); |
| 72 | + } |
| 73 | + |
65 | 74 | @SuppressWarnings("unchecked") |
66 | 75 | Map<String, String> options = (Map<String, String>) request.get("options"); |
67 | 76 | if (options == null) { |
@@ -137,4 +146,58 @@ public ResponseEntity<Map<String, Object>> getSupportedSources( |
137 | 146 | .body(Map.of("error", "Failed to get supported sources")); |
138 | 147 | } |
139 | 148 | } |
| 149 | + |
| 150 | + /** |
| 151 | + * Validate that the provided sourceUrl is safe to use for outbound HTTP(S) requests. |
| 152 | + * This helps protect against server-side request forgery (SSRF). |
| 153 | + */ |
| 154 | + private boolean isAllowedSourceUrl(String sourceUrl) { |
| 155 | + try { |
| 156 | + URI uri = new URI(sourceUrl); |
| 157 | + |
| 158 | + String scheme = uri.getScheme(); |
| 159 | + if (scheme == null || (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme))) { |
| 160 | + return false; |
| 161 | + } |
| 162 | + |
| 163 | + String host = uri.getHost(); |
| 164 | + if (host == null || host.isEmpty()) { |
| 165 | + return false; |
| 166 | + } |
| 167 | + |
| 168 | + InetAddress address = InetAddress.getByName(host); |
| 169 | + if (address.isAnyLocalAddress() || address.isLoopbackAddress()) { |
| 170 | + return false; |
| 171 | + } |
| 172 | + |
| 173 | + byte[] ip = address.getAddress(); |
| 174 | + int firstOctet = ip[0] & 0xFF; |
| 175 | + int secondOctet = ip[1] & 0xFF; |
| 176 | + |
| 177 | + // 10.0.0.0/8 |
| 178 | + if (firstOctet == 10) { |
| 179 | + return false; |
| 180 | + } |
| 181 | + // 172.16.0.0 – 172.31.255.255 |
| 182 | + if (firstOctet == 172 && secondOctet >= 16 && secondOctet <= 31) { |
| 183 | + return false; |
| 184 | + } |
| 185 | + // 192.168.0.0/16 |
| 186 | + if (firstOctet == 192 && secondOctet == 168) { |
| 187 | + return false; |
| 188 | + } |
| 189 | + // 169.254.0.0/16 (link-local) |
| 190 | + if (firstOctet == 169 && secondOctet == 254) { |
| 191 | + return false; |
| 192 | + } |
| 193 | + |
| 194 | + return true; |
| 195 | + } catch (URISyntaxException e) { |
| 196 | + log.warn("Invalid sourceUrl syntax: {}", sourceUrl, e); |
| 197 | + return false; |
| 198 | + } catch (Exception e) { |
| 199 | + log.warn("Failed to validate sourceUrl: {}", sourceUrl, e); |
| 200 | + return false; |
| 201 | + } |
| 202 | + } |
140 | 203 | } |
0 commit comments