Go: fix SSRF (#17641)

### Summary

Check the URL to prevent SSRF attack.

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
This commit is contained in:
Jin Hai
2026-07-31 19:15:38 +08:00
committed by GitHub
parent f03a00c54c
commit b058229ab3
146 changed files with 833 additions and 74 deletions

View File

@@ -203,6 +203,7 @@ func TestResolveMarkdownImage_NoImage(t *testing.T) {
}
func TestResolveMarkdownImage_HTTPImage(t *testing.T) {
withSSRFBypass(t)
// httptest servers bind loopback, which the SSRF guard rejects by
// default. Allow loopback for this test so the HTTP fetch path is
// exercised (production keeps ssrfAllowLoopback == false).
@@ -236,6 +237,7 @@ func TestFetchImageAsBase64_RejectsCredentials(t *testing.T) {
}
func TestFetchImageAsBase64_InvalidURL(t *testing.T) {
withSSRFBypass(t)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))

View File

@@ -88,7 +88,7 @@ func parsePDFWithDocling(ctx context.Context, filename string, data []byte, pars
var lastErr error
for _, candidate := range payloads {
url := baseURL + candidate.endpoint
resp, err := models.PostJSONRequest(context.Background(), models.NewDriverHTTPClient(), url, auth, candidate.body())
resp, err := models.PostJSONRequest(context.Background(), models.NewDriverHTTPClient(false), url, auth, candidate.body())
if err != nil {
lastErr = fmt.Errorf("%s: %w", candidate.endpoint, err)
continue

View File

@@ -11,6 +11,7 @@ import (
)
func TestPDFParser_ParseWithResult_DoclingChunkedMarkdownIntegration(t *testing.T) {
withSSRFBypass(t)
var requestCount atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -84,6 +85,7 @@ func TestPDFParser_ParseWithResult_DoclingChunkedMarkdownIntegration(t *testing.
}
func TestPDFParser_ParseWithResult_DoclingFallbackToStandardJSONIntegration(t *testing.T) {
withSSRFBypass(t)
var requestCount atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

View File

@@ -11,6 +11,7 @@ import (
)
func TestPDFParser_ParseWithResult_MinerUMarkdownIntegration(t *testing.T) {
withSSRFBypass(t)
var submitCalled atomic.Bool
var resultCalled atomic.Bool
@@ -100,6 +101,7 @@ func TestPDFParser_ParseWithResult_MinerUMarkdownIntegration(t *testing.T) {
}
func TestPDFParser_ParseWithResult_MinerUJSONIntegration(t *testing.T) {
withSSRFBypass(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPost && r.URL.Path == "/file_parse":

View File

@@ -41,7 +41,7 @@ func parsePDFWithOpenDataLoader(ctx context.Context, filename string, data []byt
if apiKey != "" {
req.Header.Set("Authorization", "Bearer "+apiKey)
}
resp, err := models.NewDriverHTTPClient().Do(req)
resp, err := models.NewDriverHTTPClient(false).Do(req)
if err != nil {
return ParseResult{Err: fmt.Errorf("parser: OpenDataLoader submit: %w", err)}
}

View File

@@ -10,6 +10,7 @@ import (
)
func TestPDFParser_ParseWithResult_OpenDataLoaderJSONIntegration(t *testing.T) {
withSSRFBypass(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/file_parse" {
http.NotFound(w, r)
@@ -84,6 +85,7 @@ func TestPDFParser_ParseWithResult_OpenDataLoaderJSONIntegration(t *testing.T) {
}
func TestPDFParser_ParseWithResult_OpenDataLoaderMarkdownFallback(t *testing.T) {
withSSRFBypass(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/file_parse" {
http.NotFound(w, r)

View File

@@ -11,6 +11,7 @@ import (
)
func TestPDFParser_ParseWithResult_PaddleOCRMarkdownIntegration(t *testing.T) {
withSSRFBypass(t)
var called atomic.Bool
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -82,6 +83,7 @@ func TestPDFParser_ParseWithResult_PaddleOCRMarkdownIntegration(t *testing.T) {
}
func TestPDFParser_ParseWithResult_PaddleOCRJSONIntegration(t *testing.T) {
withSSRFBypass(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/layout-parsing" {
http.NotFound(w, r)

View File

@@ -84,7 +84,7 @@ func soMarkSubmit(baseURL, filename string, data []byte, parser *PDFParser, apiK
return "", fmt.Errorf("parser: SoMark request: %w", err)
}
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err := models.NewDriverHTTPClient().Do(req)
resp, err := models.NewDriverHTTPClient(false).Do(req)
if err != nil {
return "", fmt.Errorf("parser: SoMark submit: %w", err)
}
@@ -122,7 +122,7 @@ func soMarkPoll(baseURL, taskID, apiKey string) (map[string]any, error) {
return nil, fmt.Errorf("parser: SoMark poll request: %w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := models.NewDriverHTTPClient().Do(req)
resp, err := models.NewDriverHTTPClient(false).Do(req)
if err != nil {
return nil, fmt.Errorf("parser: SoMark poll: %w", err)
}

View File

@@ -10,6 +10,7 @@ import (
)
func TestPDFParser_ParseWithResult_SoMarkJSONIntegration(t *testing.T) {
withSSRFBypass(t)
var submitSeen bool
var pollSeen bool
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -78,6 +79,7 @@ func TestPDFParser_ParseWithResult_SoMarkJSONIntegration(t *testing.T) {
}
func TestPDFParser_ParseWithResult_SoMarkMarkdownIntegration(t *testing.T) {
withSSRFBypass(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/parse/async":
@@ -123,6 +125,7 @@ func TestSoMarkBlockToItem_DropsHeaderByDefault(t *testing.T) {
}
func TestSoMarkSubmitMultipartShape(t *testing.T) {
withSSRFBypass(t)
var form multipart.Form
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = r.ParseMultipartForm(1 << 20)

View File

@@ -40,7 +40,7 @@ func parsePDFWithTCADP(filename string, data []byte, parser *PDFParser) ParseRes
"MarkdownImageResponseType": parser.TCADPMarkdownImageResponseType,
},
}
resp, err := models.PostJSONRequest(context.Background(), models.NewDriverHTTPClient(), strings.TrimRight(baseURL, "/")+"/reconstruct_document", bearer(apiKey), requestBody)
resp, err := models.PostJSONRequest(context.Background(), models.NewDriverHTTPClient(false), strings.TrimRight(baseURL, "/")+"/reconstruct_document", bearer(apiKey), requestBody)
if err != nil {
return ParseResult{Err: fmt.Errorf("parser: TCADP submit: %w", err)}
}
@@ -68,7 +68,7 @@ func parsePDFWithTCADP(filename string, data []byte, parser *PDFParser) ParseRes
if auth := bearer(apiKey); auth != "" {
downloadReq.Header.Set("Authorization", auth)
}
downloadResp, err := models.NewDriverHTTPClient().Do(downloadReq)
downloadResp, err := models.NewDriverHTTPClient(false).Do(downloadReq)
if err != nil {
return ParseResult{Err: fmt.Errorf("parser: TCADP download: %w", err)}
}

View File

@@ -10,6 +10,7 @@ import (
)
func TestPDFParser_ParseWithResult_TCADPJSONIntegration(t *testing.T) {
withSSRFBypass(t)
zipPayload := tcadpZipFixture(t)
var server *httptest.Server
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -47,6 +48,7 @@ func TestPDFParser_ParseWithResult_TCADPJSONIntegration(t *testing.T) {
}
func TestPDFParser_ParseWithResult_TCADPMarkdownIntegration(t *testing.T) {
withSSRFBypass(t)
zipPayload := tcadpZipFixture(t)
var server *httptest.Server
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

View File

@@ -135,6 +135,7 @@ type tcadpPresentationParser interface {
}
func testPresentationTCADPIntegration(t *testing.T, p tcadpPresentationParser, wantFileType, filename string) {
withSSRFBypass(t)
t.Helper()
zipPayload := tcadpZipFixture(t)
var gotFileType string
@@ -189,6 +190,7 @@ func testPresentationTCADPIntegration(t *testing.T, p tcadpPresentationParser, w
// from the TCADP download endpoint is surfaced as an explicit error rather
// than parsed as a (malformed) ZIP artifact.
func TestPPTXParser_TCADPDownloadHTTPError(t *testing.T) {
withSSRFBypass(t)
var server *httptest.Server
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {

View File

@@ -45,7 +45,7 @@ func parsePresentationWithTCADP(ctx context.Context, filename string, data []byt
"MarkdownImageResponseType": markdownImageResponseType,
},
}
resp, err := models.PostJSONRequest(ctx, models.NewDriverHTTPClient(),
resp, err := models.PostJSONRequest(ctx, models.NewDriverHTTPClient(false),
strings.TrimRight(baseURL, "/")+"/reconstruct_document", bearer(apiKey), requestBody)
if err != nil {
return ParseResult{Err: fmt.Errorf("parser: TCADP submit: %w", err)}
@@ -75,7 +75,7 @@ func parsePresentationWithTCADP(ctx context.Context, filename string, data []byt
if auth := bearer(apiKey); auth != "" {
downloadReq.Header.Set("Authorization", auth)
}
downloadResp, err := models.NewDriverHTTPClient().Do(downloadReq)
downloadResp, err := models.NewDriverHTTPClient(false).Do(downloadReq)
if err != nil {
return ParseResult{Err: fmt.Errorf("parser: TCADP download: %w", err)}
}

View File

@@ -0,0 +1,19 @@
package parser
import (
"testing"
"ragflow/internal/utility"
)
// withSSRFBypass enables the test-only SSRF bypass for the duration of a test
// and restores the previous value in t.Cleanup. Parser tests exercise parsing
// logic against httptest servers bound to 127.0.0.1, which the strict SSRF
// guard rejects. SSRF enforcement has its own dedicated coverage in
// internal/utility/ssrf_test.go.
func withSSRFBypass(t *testing.T) {
t.Helper()
prev := utility.AllowAnyHostForTest
utility.AllowAnyHostForTest = true
t.Cleanup(func() { utility.AllowAnyHostForTest = prev })
}

View File

@@ -38,7 +38,7 @@ func parseSpreadsheetWithTCADP(filename string, data []byte, fileType string, tc
"MarkdownImageResponseType": markdownImageResponseType,
},
}
resp, err := models.PostJSONRequest(context.Background(), models.NewDriverHTTPClient(), strings.TrimRight(baseURL, "/")+"/reconstruct_document", bearer(apiKey), requestBody)
resp, err := models.PostJSONRequest(context.Background(), models.NewDriverHTTPClient(false), strings.TrimRight(baseURL, "/")+"/reconstruct_document", bearer(apiKey), requestBody)
if err != nil {
return ParseResult{Err: fmt.Errorf("parser: TCADP submit: %w", err)}
}
@@ -66,7 +66,7 @@ func parseSpreadsheetWithTCADP(filename string, data []byte, fileType string, tc
if auth := bearer(apiKey); auth != "" {
downloadReq.Header.Set("Authorization", auth)
}
downloadResp, err := models.NewDriverHTTPClient().Do(downloadReq)
downloadResp, err := models.NewDriverHTTPClient(false).Do(downloadReq)
if err != nil {
return ParseResult{Err: fmt.Errorf("parser: TCADP download: %w", err)}
}

View File

@@ -7,6 +7,7 @@ import (
)
func TestXLSXParser_ParseWithResult_TCADPJSONIntegration(t *testing.T) {
withSSRFBypass(t)
ctx := t.Context()
zipPayload := tcadpZipFixture(t)
var server *httptest.Server
@@ -45,6 +46,7 @@ func TestXLSXParser_ParseWithResult_TCADPJSONIntegration(t *testing.T) {
}
func TestXLSParser_ParseWithResult_TCADPJSONIntegration(t *testing.T) {
withSSRFBypass(t)
ctx := t.Context()
zipPayload := tcadpZipFixture(t)
var server *httptest.Server
@@ -80,6 +82,7 @@ func TestXLSParser_ParseWithResult_TCADPJSONIntegration(t *testing.T) {
}
func TestCSVParser_ParseWithResult_TCADPJSONIntegration(t *testing.T) {
withSSRFBypass(t)
ctx := t.Context()
zipPayload := tcadpZipFixture(t)
var server *httptest.Server
@@ -112,6 +115,7 @@ func TestCSVParser_ParseWithResult_TCADPJSONIntegration(t *testing.T) {
}
func TestXLSXParser_ParseWithResult_TCADPMarkdownIntegration(t *testing.T) {
withSSRFBypass(t)
ctx := t.Context()
zipPayload := tcadpZipFixture(t)
var server *httptest.Server