mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-14 11:37:31 +08:00
fix(browser): actually block redirect targets in SSRF interceptor
The first cut of the per-request interceptor called route.resume() for every request. Playwright follows server-side 3xx redirects internally on resume() WITHOUT re-invoking the route handler, so a public page that 302s to a metadata IP still reached it — verified via runtime E2E (the handler only ever saw the httpbin.org URLs, never the 169.254.169.254 redirect target). Fix: for navigation requests, fetch with maxRedirects=0 and validate the Location of each hop through UrlSafetyChecker before fulfilling; abort when a hop resolves to a blocked host. Subresources/fetches keep the direct per-URL check + resume path. Non-navigation and non-http(s) requests are unaffected. Runtime-verified: httpbin.org 302 -> 169.254.169.254 is now aborted (net::ERR_FAILED; log "blocked redirect ... cloud-metadata endpoint"), while example.com and wikipedia.org (rich subresources) still load with no false blocks.
This commit is contained in:
parent
c95df54949
commit
c3da1ce817
@ -1,5 +1,6 @@
|
|||||||
package vip.mate.tool.browser;
|
package vip.mate.tool.browser;
|
||||||
|
|
||||||
|
import com.microsoft.playwright.APIResponse;
|
||||||
import com.microsoft.playwright.Browser;
|
import com.microsoft.playwright.Browser;
|
||||||
import com.microsoft.playwright.BrowserContext;
|
import com.microsoft.playwright.BrowserContext;
|
||||||
import com.microsoft.playwright.BrowserType;
|
import com.microsoft.playwright.BrowserType;
|
||||||
@ -10,6 +11,8 @@ import com.microsoft.playwright.Route;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import vip.mate.common.net.SsrfProperties;
|
import vip.mate.common.net.SsrfProperties;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
@ -390,11 +393,17 @@ public class BrowserLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Re-run the SSRF guard on every http(s) request the page makes, so redirects,
|
* Re-run the SSRF guard on every http(s) request the page makes, so subresources,
|
||||||
* subresources and script-initiated fetches cannot reach blocked hosts (above
|
* script-initiated fetches AND server-side redirect targets cannot reach blocked
|
||||||
* all cloud-metadata endpoints) after the initial navigation URL already passed
|
* hosts (above all cloud-metadata endpoints) after the initial navigation URL
|
||||||
* the one-shot check in the tool layer. Non-network schemes (data:, blob:,
|
* already passed the one-shot check in the tool layer.
|
||||||
* about:, …) are not SSRF vectors and pass through untouched.
|
* <p>
|
||||||
|
* Redirects need special handling: Playwright follows 3xx responses internally on
|
||||||
|
* {@code route.resume()} without re-invoking this handler, so a public page that
|
||||||
|
* 302s to a metadata IP would slip through. For navigation requests we therefore
|
||||||
|
* fetch with {@code maxRedirects=0} and validate the {@code Location} of every hop
|
||||||
|
* before fulfilling, so each redirect target is checked. Non-network schemes
|
||||||
|
* (data:, blob:, about:, …) are not SSRF vectors and pass through untouched.
|
||||||
*/
|
*/
|
||||||
private void installSsrfInterceptor(BrowserContext context) {
|
private void installSsrfInterceptor(BrowserContext context) {
|
||||||
if (!props.isSsrfCheckEnabled()) {
|
if (!props.isSsrfCheckEnabled()) {
|
||||||
@ -407,19 +416,54 @@ public class BrowserLauncher {
|
|||||||
route.resume();
|
route.resume();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 1. Validate the request URL itself (covers subresources and fetches,
|
||||||
|
// which are each delivered to this handler as their own request).
|
||||||
try {
|
try {
|
||||||
UrlSafetyChecker.check(reqUrl, ssrfProperties.getSsrfAllowlist(),
|
UrlSafetyChecker.check(reqUrl, ssrfProperties.getSsrfAllowlist(),
|
||||||
props.isAllowPrivateNetwork());
|
props.isAllowPrivateNetwork());
|
||||||
route.resume();
|
|
||||||
} catch (SecurityException se) {
|
} catch (SecurityException se) {
|
||||||
log.warn("[BrowserLauncher] SSRF interceptor blocked request url={}: {}",
|
log.warn("[BrowserLauncher] SSRF interceptor blocked request url={}: {}",
|
||||||
reqUrl, se.getMessage());
|
reqUrl, se.getMessage());
|
||||||
route.abort();
|
route.abort();
|
||||||
|
return;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Unexpected checker/routing error — the initial navigation URL was
|
|
||||||
// already validated, so let the request proceed rather than wedging
|
|
||||||
// the page on a transient fault.
|
|
||||||
route.resume();
|
route.resume();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 2. Non-navigation requests carry no auto-followed redirect chain, so the
|
||||||
|
// URL check above is sufficient — resume normally.
|
||||||
|
if (!route.request().isNavigationRequest()) {
|
||||||
|
route.resume();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 3. Navigation requests: follow redirects manually so each hop is checked.
|
||||||
|
try {
|
||||||
|
APIResponse resp = route.fetch(new Route.FetchOptions().setMaxRedirects(0));
|
||||||
|
int status = resp.status();
|
||||||
|
if (status >= 300 && status < 400) {
|
||||||
|
String location = resp.headers().get("location");
|
||||||
|
if (location != null && !location.isBlank()) {
|
||||||
|
String target = URI.create(reqUrl).resolve(location.trim()).toString();
|
||||||
|
UrlSafetyChecker.check(target, ssrfProperties.getSsrfAllowlist(),
|
||||||
|
props.isAllowPrivateNetwork());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Hand the (possibly-3xx) response back; the browser follows any safe
|
||||||
|
// redirect, whose next hop re-enters this handler and is re-validated.
|
||||||
|
route.fulfill(new Route.FulfillOptions().setResponse(resp));
|
||||||
|
} catch (SecurityException se) {
|
||||||
|
log.warn("[BrowserLauncher] SSRF interceptor blocked redirect from url={}: {}",
|
||||||
|
reqUrl, se.getMessage());
|
||||||
|
route.abort();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Manual fetch/proxy failed (network, unsupported response, …). The
|
||||||
|
// request URL itself already passed the check, so fall back to normal
|
||||||
|
// handling rather than wedging the page.
|
||||||
|
try {
|
||||||
|
route.resume();
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
// route may already be consumed by the failed fetch; nothing to do.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user