[OCaml] Mobile-friendly clone of cgit.
fix Sandbox raw blob responses
Serve raw blobs with Content-Security-Policy: sandbox and X-Content-Type-Options: nosniff. Without them, an SVG pushed to a hosted repository could run script with the site authority when served inline as image/svg+xml.
Changed files
lib/handlers.ml
@@ -269,6 +269,17 @@
269
269
| ".avif" -> "image/avif"
270
270
| _ -> "text/plain; charset=utf-8"
271
271
272
Added:
(* Raw blobs are repository content served from ogit's own origin. The
273
Added:
sandbox policy stops any active content — an SVG carrying a script is the
274
Added:
canonical case — from running with the site's authority, and [nosniff]
275
Added:
stops browsers from promoting text/plain to something executable. *)
276
Added:
let raw_headers content_type =
277
Added:
[
278
Added:
("Content-Type", content_type);
279
Added:
("Content-Security-Policy", "sandbox");
280
Added:
("X-Content-Type-Options", "nosniff");
281
Added:
]
282
Added:
272
283
let raw_file repository _context id =
273
284
let* trail = Resolvers.Tree.find_path repository id in
274
285
let* object_ = Resolvers.blob_or_tree repository id in
@@ -280,9 +291,7 @@
280
291
| [] -> "text/plain; charset=utf-8"
281
292
in
282
293
Lwt.return
283
Removed:
(Dream.response
284
Removed:
~headers:[ ("Content-Type", content_type) ]
285
Removed:
blob.content)
294
Added:
(Dream.response ~headers:(raw_headers content_type) blob.content)
286
295
| `Tree _ ->
287
296
error_response (Resolvers.Bad_request "object is a tree, not a blob")
288
297
end
test/test_router.ml
@@ -59,6 +59,41 @@
59
59
let status = Dream.request ~target "" |> request |> Dream.status in
60
60
Alcotest.(check int) "404" 404 (Dream.status_to_int status))
61
61
62
Added:
(* Raw responses must never let repository content run with the site's
63
Added:
authority: the sandbox policy and nosniff header are the contract. An SVG is
64
Added:
used because it is the content type that made the headers necessary. *)
65
Added:
let test_raw_response_headers () =
66
Added:
with_temp_directory "ogit-router" (fun root ->
67
Added:
let name = "project" in
68
Added:
let path = Filename.concat root name in
69
Added:
Unix.mkdir path 0o755;
70
Added:
ignore (git [ "-C"; path; "init"; "-q"; "-b"; "main" ]);
71
Added:
ignore (git [ "-C"; path; "config"; "user.name"; "Test" ]);
72
Added:
ignore (git [ "-C"; path; "config"; "user.email"; "t@t.invalid" ]);
73
Added:
Out_channel.with_open_text (Filename.concat path "img.svg") (fun ch ->
74
Added:
output_string ch "<svg xmlns=\"http://www.w3.org/2000/svg\"/>\n");
75
Added:
ignore (git [ "-C"; path; "add"; "." ]);
76
Added:
ignore (git [ "-C"; path; "commit"; "-q"; "-m"; "init" ]);
77
Added:
let blob_hash = git [ "-C"; path; "rev-parse"; "HEAD:img.svg" ] in
78
Added:
let config = Ogit.Config.{ default with git_project_root = root } in
79
Added:
let request = Dream.test (Dream.router (Ogit.Handlers.routes config)) in
80
Added:
let target = "/project/raw/" ^ blob_hash in
81
Added:
let response = Dream.request ~target "" |> request in
82
Added:
Alcotest.(check int)
83
Added:
"200" 200
84
Added:
(Dream.status_to_int (Dream.status response));
85
Added:
let header name =
86
Added:
Dream.header response name |> Option.value ~default:"<missing>"
87
Added:
in
88
Added:
Alcotest.(check string)
89
Added:
"content type" "image/svg+xml" (header "Content-Type");
90
Added:
Alcotest.(check string)
91
Added:
"sandboxed" "sandbox"
92
Added:
(header "Content-Security-Policy");
93
Added:
Alcotest.(check string)
94
Added:
"nosniff" "nosniff"
95
Added:
(header "X-Content-Type-Options"))
96
Added:
62
97
let suite =
63
98
( "router",
64
99
[
@@ -66,4 +101,5 @@
66
101
Alcotest.test_case "missing repo" `Slow test_missing_repo;
67
102
Alcotest.test_case "invalid hash" `Slow test_invalid_hash;
68
103
Alcotest.test_case "missing object" `Slow test_missing_object;
104
Added:
Alcotest.test_case "raw response headers" `Slow test_raw_response_headers;
69
105
] )