仰望星辰工作室

gitcat

gitcat/ internal/server/raw_test.go 3.7 KB · 108 行 原始文件
1package server
2
3import (
4 "net/http"
5 "net/http/httptest"
6 "strings"
7 "testing"
8)
9
10// TestRawEndpointHardening 是 P0-3 的回归测试:raw 端点不得把仓库里的可执行
11// 内容以内联方式返回。修复前 evil.html 会带着 text/html + inline 在 gitcat
12// 自己的源下渲染,提交一个文件等于对所有访客执行脚本。
13func TestRawEndpointHardening(t *testing.T) {
14 srv, st, _, _ := newTestServer(t)
15 dir := makeRepo(t, srv, st, "files", nil)
16 commitFiles(t, dir, "add files", map[string]string{
17 "evil.html": `<script>alert(document.domain)</script>`,
18 "logo.svg": `<svg><script>alert(1)</script></svg>`,
19 "notes.txt": "hello",
20 "pic.png": "\x89PNG\r\n\x1a\nfake",
21 })
22
23 cases := []struct {
24 file string
25 wantDisp string
26 denyHTML bool
27 }{
28 {"evil.html", "attachment", true},
29 {"logo.svg", "attachment", true},
30 {"notes.txt", "inline", false},
31 {"pic.png", "inline", false},
32 }
33 for _, tc := range cases {
34 t.Run(tc.file, func(t *testing.T) {
35 r := httptest.NewRequest(http.MethodGet, "/files/raw/main/"+tc.file, nil)
36 w := httptest.NewRecorder()
37 srv.Handler().ServeHTTP(w, r)
38 if w.Code != http.StatusOK {
39 t.Fatalf("状态码 = %d,期望 200", w.Code)
40 }
41 ct := w.Header().Get("Content-Type")
42 if disp := w.Header().Get("Content-Disposition"); !strings.Contains(disp, tc.wantDisp) {
43 t.Errorf("Content-Disposition = %q,期望包含 %q", disp, tc.wantDisp)
44 }
45 if tc.denyHTML && (strings.Contains(ct, "text/html") || strings.Contains(ct, "svg")) {
46 t.Errorf("可执行类型被原样返回: Content-Type = %q", ct)
47 }
48 if got := w.Header().Get("Content-Security-Policy"); !strings.Contains(got, "sandbox") {
49 t.Errorf("缺少 CSP sandbox,实际 = %q", got)
50 }
51 if got := w.Header().Get("X-Content-Type-Options"); got != "nosniff" {
52 t.Errorf("X-Content-Type-Options = %q,期望 nosniff", got)
53 }
54 })
55 }
56}
57
58// TestRawRejectsOptionInjection 确认 ref 参数无法被 git 当成选项。
59func TestRawRejectsOptionInjection(t *testing.T) {
60 srv, st, _, _ := newTestServer(t)
61 makeRepo(t, srv, st, "safe", nil)
62 for _, ref := range []string{"--upload-pack=evil", "-x", "a%20b", "a..b", "!bang"} {
63 r := httptest.NewRequest(http.MethodGet, "/safe/raw/"+ref+"/f.txt", nil)
64 w := httptest.NewRecorder()
65 srv.Handler().ServeHTTP(w, r)
66 if w.Code == http.StatusOK {
67 t.Errorf("非法 ref %q 被放行", ref)
68 }
69 }
70}
71
72// TestPagesHaveCSP 确认所有页面都带上了 CSP。
73func TestPagesHaveCSP(t *testing.T) {
74 srv, st, _, _ := newTestServer(t)
75 makeRepo(t, srv, st, "proj", nil)
76 commitFiles(t, srv.repoPath("proj"), "init", map[string]string{"a.txt": "hi"})
77
78 for _, path := range []string{"/", "/proj", "/login"} {
79 r := httptest.NewRequest(http.MethodGet, path, nil)
80 w := httptest.NewRecorder()
81 srv.Handler().ServeHTTP(w, r)
82 if got := w.Header().Get("Content-Security-Policy"); !strings.Contains(got, "default-src 'self'") {
83 t.Errorf("%s 缺少 CSP: %q", path, got)
84 }
85 }
86}
87
88// TestBlobSkipsOversizedFile 确认超大文件不进内存(只渲染下载入口)。
89func TestBlobSkipsOversizedFile(t *testing.T) {
90 srv, st, _, _ := newTestServer(t)
91 dir := makeRepo(t, srv, st, "big", nil)
92 big := strings.Repeat("A", 3<<20) // 3MB > 2MB 预览上限
93 commitFiles(t, dir, "big file", map[string]string{"big.txt": big})
94
95 r := httptest.NewRequest(http.MethodGet, "/big/blob/main/big.txt", nil)
96 w := httptest.NewRecorder()
97 srv.Handler().ServeHTTP(w, r)
98 if w.Code != http.StatusOK {
99 t.Fatalf("状态码 = %d", w.Code)
100 }
101 body := w.Body.String()
102 if !strings.Contains(body, "超过 2 MB") {
103 t.Error("超大文件应显示「跳过预览」提示")
104 }
105 if strings.Contains(body, "codeline") {
106 t.Error("超大文件不应渲染代码行")
107 }
108}