package server import ( "net/http" "net/http/httptest" "strings" "testing" ) // TestRawEndpointHardening 是 P0-3 的回归测试:raw 端点不得把仓库里的可执行 // 内容以内联方式返回。修复前 evil.html 会带着 text/html + inline 在 gitcat // 自己的源下渲染,提交一个文件等于对所有访客执行脚本。 func TestRawEndpointHardening(t *testing.T) { srv, st, _, _ := newTestServer(t) dir := makeRepo(t, srv, st, "files", nil) commitFiles(t, dir, "add files", map[string]string{ "evil.html": ``, "logo.svg": ``, "notes.txt": "hello", "pic.png": "\x89PNG\r\n\x1a\nfake", }) cases := []struct { file string wantDisp string denyHTML bool }{ {"evil.html", "attachment", true}, {"logo.svg", "attachment", true}, {"notes.txt", "inline", false}, {"pic.png", "inline", false}, } for _, tc := range cases { t.Run(tc.file, func(t *testing.T) { r := httptest.NewRequest(http.MethodGet, "/files/raw/main/"+tc.file, nil) w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, r) if w.Code != http.StatusOK { t.Fatalf("状态码 = %d,期望 200", w.Code) } ct := w.Header().Get("Content-Type") if disp := w.Header().Get("Content-Disposition"); !strings.Contains(disp, tc.wantDisp) { t.Errorf("Content-Disposition = %q,期望包含 %q", disp, tc.wantDisp) } if tc.denyHTML && (strings.Contains(ct, "text/html") || strings.Contains(ct, "svg")) { t.Errorf("可执行类型被原样返回: Content-Type = %q", ct) } if got := w.Header().Get("Content-Security-Policy"); !strings.Contains(got, "sandbox") { t.Errorf("缺少 CSP sandbox,实际 = %q", got) } if got := w.Header().Get("X-Content-Type-Options"); got != "nosniff" { t.Errorf("X-Content-Type-Options = %q,期望 nosniff", got) } }) } } // TestRawRejectsOptionInjection 确认 ref 参数无法被 git 当成选项。 func TestRawRejectsOptionInjection(t *testing.T) { srv, st, _, _ := newTestServer(t) makeRepo(t, srv, st, "safe", nil) for _, ref := range []string{"--upload-pack=evil", "-x", "a%20b", "a..b", "!bang"} { r := httptest.NewRequest(http.MethodGet, "/safe/raw/"+ref+"/f.txt", nil) w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, r) if w.Code == http.StatusOK { t.Errorf("非法 ref %q 被放行", ref) } } } // TestPagesHaveCSP 确认所有页面都带上了 CSP。 func TestPagesHaveCSP(t *testing.T) { srv, st, _, _ := newTestServer(t) makeRepo(t, srv, st, "proj", nil) commitFiles(t, srv.repoPath("proj"), "init", map[string]string{"a.txt": "hi"}) for _, path := range []string{"/", "/proj", "/login"} { r := httptest.NewRequest(http.MethodGet, path, nil) w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, r) if got := w.Header().Get("Content-Security-Policy"); !strings.Contains(got, "default-src 'self'") { t.Errorf("%s 缺少 CSP: %q", path, got) } } } // TestBlobSkipsOversizedFile 确认超大文件不进内存(只渲染下载入口)。 func TestBlobSkipsOversizedFile(t *testing.T) { srv, st, _, _ := newTestServer(t) dir := makeRepo(t, srv, st, "big", nil) big := strings.Repeat("A", 3<<20) // 3MB > 2MB 预览上限 commitFiles(t, dir, "big file", map[string]string{"big.txt": big}) r := httptest.NewRequest(http.MethodGet, "/big/blob/main/big.txt", nil) w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, r) if w.Code != http.StatusOK { t.Fatalf("状态码 = %d", w.Code) } body := w.Body.String() if !strings.Contains(body, "超过 2 MB") { t.Error("超大文件应显示「跳过预览」提示") } if strings.Contains(body, "codeline") { t.Error("超大文件不应渲染代码行") } }