Skip to content

Commit cfb21e8

Browse files
committed
🐛 Limit forward proxy response body size #18991
1 parent 1d57a3f commit cfb21e8

5 files changed

Lines changed: 101 additions & 11 deletions

File tree

docs/API.ja.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1624,6 +1624,8 @@
16241624
* `hex`
16251625

16261626
`text`は既存の動作を維持し、該当する場合は文字コードをUTF-8に変換します。バイナリエンコーディングは文字コード変換前のレスポンス本文データに適用され、gzip展開など既存のHTTPコンテンツデコード動作は変わりません。
1627+
1628+
HTTPコンテンツデコード後のレスポンス本文は32 MiBに制限されます。上限を超えた場合、部分的な本文を返さずにエラーコード`10`を返します。大きなファイルやストリーミングレスポンスには`/api/network/proxy`を使用してください。
16271629
* 戻り値
16281630

16291631
```json

docs/API.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1630,6 +1630,8 @@ Note: To ensure data security, access to this interface is prohibited in Publish
16301630
* `hex`
16311631

16321632
`text` preserves the existing behavior and converts the character set to UTF-8 when applicable. The binary encodings encode the response body before character-set conversion; existing HTTP content decoding behavior, such as gzip decompression, is unchanged.
1633+
1634+
The response body is limited to 32 MiB after HTTP content decoding. If the limit is exceeded, the API returns error code `10` without a partial body. Use `/api/network/proxy` for large files or streaming responses.
16331635
* Return value
16341636

16351637
```json

docs/API.zh-CN.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1624,6 +1624,8 @@
16241624
* `hex`
16251625

16261626
`text` 保持现有行为,在适用时将字符集转换为 UTF-8。二进制编码作用于字符集转换前的响应正文数据;gzip 解压等现有 HTTP 内容解码行为不变。
1627+
1628+
HTTP 内容解码后的响应正文上限为 32 MiB,超限时返回错误码 `10` 且不返回部分正文。大文件或流式响应请使用 `/api/network/proxy`
16271629
* 返回值
16281630

16291631
```json

kernel/api/network.go

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"encoding/base64"
2222
"encoding/hex"
2323
"encoding/json"
24+
"errors"
2425
"fmt"
2526
"io"
2627
"net/http"
@@ -37,6 +38,8 @@ import (
3738
"github.com/siyuan-note/siyuan/kernel/util"
3839
)
3940

41+
const maxForwardProxyResponseSize int64 = 32 * 1024 * 1024
42+
4043
type File struct {
4144
Filename string
4245
Header textproto.MIMEHeader
@@ -186,7 +189,7 @@ func forwardProxy(c *gin.Context) {
186189
}
187190

188191
client := getSafeClient(time.Duration(timeout) * time.Millisecond)
189-
responseEncoding := configureForwardProxyResponseEncoding(client, arg["responseEncoding"])
192+
responseEncoding := configureForwardProxyClient(client, maxForwardProxyResponseSize, arg["responseEncoding"])
190193
if redirectArg, ok := arg["redirect"].(bool); ok && !redirectArg {
191194
client.SetRedirectPolicy(req.NoRedirectPolicy())
192195
}
@@ -263,17 +266,15 @@ func forwardProxy(c *gin.Context) {
263266
}
264267

265268
started := time.Now()
266-
resp, err := request.Send(method, destURL)
267-
if err != nil {
268-
ret.Code = 8
269-
ret.Msg = "forward request failed: " + err.Error()
269+
resp, bodyData, err := sendForwardProxyRequest(request, method, destURL)
270+
if errors.Is(err, req.ErrResponseBodyTooLarge) {
271+
ret.Code = 10
272+
ret.Msg = fmt.Sprintf("response body too large: limit is %d bytes", maxForwardProxyResponseSize)
270273
return
271274
}
272-
273-
bodyData, err := io.ReadAll(resp.Body)
274275
if err != nil {
275-
ret.Code = 9
276-
ret.Msg = "read response body failed: " + err.Error()
276+
ret.Code = 8
277+
ret.Msg = "forward request failed: " + err.Error()
277278
return
278279
}
279280

@@ -324,7 +325,9 @@ func forwardProxy(c *gin.Context) {
324325
// elapsed.Seconds(), len(bodyData), data["url"], headers, contentType, arg["payload"], data["status"], shortBody)
325326
}
326327

327-
func configureForwardProxyResponseEncoding(client *req.Client, value any) string {
328+
func configureForwardProxyClient(client *req.Client, maxResponseSize int64, value any) string {
329+
client.SetMaxResponseSize(maxResponseSize)
330+
328331
responseEncoding, ok := value.(string)
329332
if !ok {
330333
return "text"
@@ -339,6 +342,15 @@ func configureForwardProxyResponseEncoding(client *req.Client, value any) string
339342
}
340343
}
341344

345+
func sendForwardProxyRequest(request *req.Request, method, destURL string) (response *req.Response, body []byte, err error) {
346+
response, err = request.Send(method, destURL)
347+
if err != nil {
348+
return
349+
}
350+
body = response.Bytes()
351+
return
352+
}
353+
342354
// 创建安全的 HTTP Client,防止 SSRF 和 DNS 重绑定
343355
func getSafeClient(timeout time.Duration) *req.Client {
344356
dialer := util.SSRFSafeDialer(timeout)

kernel/api/network_test.go

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ package api
22

33
import (
44
"bytes"
5+
"compress/gzip"
6+
"errors"
57
"io"
68
"net/http"
79
"net/http/httptest"
10+
"strconv"
811
"testing"
912

1013
"github.com/imroc/req/v3"
@@ -43,7 +46,7 @@ func TestConfigureForwardProxyResponseEncoding(t *testing.T) {
4346
for _, test := range tests {
4447
t.Run(test.name, func(t *testing.T) {
4548
client := req.C()
46-
if encoding := configureForwardProxyResponseEncoding(client, test.value); encoding != test.wantEncoding {
49+
if encoding := configureForwardProxyClient(client, maxForwardProxyResponseSize, test.value); encoding != test.wantEncoding {
4750
t.Fatalf("encoding = %q, want %q", encoding, test.wantEncoding)
4851
}
4952

@@ -61,3 +64,72 @@ func TestConfigureForwardProxyResponseEncoding(t *testing.T) {
6164
})
6265
}
6366
}
67+
68+
func TestForwardProxyResponseSizeLimit(t *testing.T) {
69+
const limit int64 = 64
70+
body := bytes.Repeat([]byte("a"), int(limit)+1)
71+
var compressed bytes.Buffer
72+
gzipWriter := gzip.NewWriter(&compressed)
73+
if _, err := gzipWriter.Write(body); err != nil {
74+
t.Fatalf("compress response body failed: %s", err)
75+
}
76+
if err := gzipWriter.Close(); err != nil {
77+
t.Fatalf("close gzip writer failed: %s", err)
78+
}
79+
if int64(compressed.Len()) >= limit {
80+
t.Fatalf("compressed response body size = %d, want less than %d", compressed.Len(), limit)
81+
}
82+
83+
tests := []struct {
84+
name string
85+
responseEncoding any
86+
handler http.HandlerFunc
87+
}{
88+
{
89+
name: "content length",
90+
responseEncoding: "text",
91+
handler: func(writer http.ResponseWriter, _ *http.Request) {
92+
writer.Header().Set("Content-Length", strconv.Itoa(len(body)))
93+
_, _ = writer.Write(body)
94+
},
95+
},
96+
{
97+
name: "chunked",
98+
responseEncoding: "base64",
99+
handler: func(writer http.ResponseWriter, _ *http.Request) {
100+
writer.WriteHeader(http.StatusOK)
101+
writer.(http.Flusher).Flush()
102+
_, _ = writer.Write(body)
103+
},
104+
},
105+
{
106+
name: "compressed",
107+
responseEncoding: "hex",
108+
handler: func(writer http.ResponseWriter, _ *http.Request) {
109+
writer.Header().Set("Content-Encoding", "gzip")
110+
writer.Header().Set("Content-Length", strconv.Itoa(compressed.Len()))
111+
_, _ = writer.Write(compressed.Bytes())
112+
},
113+
},
114+
}
115+
116+
for _, test := range tests {
117+
t.Run(test.name, func(t *testing.T) {
118+
server := httptest.NewServer(test.handler)
119+
defer server.Close()
120+
121+
client := req.C()
122+
configureForwardProxyClient(client, limit, test.responseEncoding)
123+
response, responseBody, err := sendForwardProxyRequest(client.R(), http.MethodGet, server.URL)
124+
if !errors.Is(err, req.ErrResponseBodyTooLarge) {
125+
t.Fatalf("error = %v, want ErrResponseBodyTooLarge", err)
126+
}
127+
if response == nil {
128+
t.Fatal("response is nil")
129+
}
130+
if responseBody != nil {
131+
t.Fatalf("response body = %x, want nil", responseBody)
132+
}
133+
})
134+
}
135+
}

0 commit comments

Comments
 (0)