fix: bound document nesting and escape generated urls
This commit is contained in:
parent
c29364d8e0
commit
49a63503d0
10 changed files with 276 additions and 19 deletions
|
|
@ -29,6 +29,10 @@ impl Server {
|
|||
write(dir.path(), "one/.itsybitsy.toml", "[defaults]\nmax_age = 120\n");
|
||||
// A line starting with a dot, which Gopher has to double.
|
||||
write(dir.path(), "one/dotted.md", "# Dotted\n\n .hidden\n");
|
||||
// Nests past the parser's cap: this used to abort the whole process.
|
||||
write(dir.path(), "one/abyss.md", &"> ".repeat(4000));
|
||||
// A real file name carrying a CR LF, which a redirect target must escape.
|
||||
write(dir.path(), "one/split\r\nX-Injected: yes.md", "# Split\n");
|
||||
write(dir.path(), "two/index.md", "# Two\n\nSecond site.\n");
|
||||
write(
|
||||
dir.path(),
|
||||
|
|
@ -500,6 +504,42 @@ fn an_empty_nex_selector_is_the_root() {
|
|||
assert!(server.send("nex", b"\r\n").starts_with("# Two"));
|
||||
}
|
||||
|
||||
// -- Robustness ----------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn a_document_nested_past_the_cap_is_an_error_and_the_server_survives() {
|
||||
// Every walk over a parsed document recurses, and a stack overflow aborts
|
||||
// the process rather than unwinding into the handler's catch_unwind, so this
|
||||
// would take down every virtual host at once.
|
||||
let server = Server::start();
|
||||
assert_eq!(split(&server.get("one.test", "/abyss")).0, "HTTP/1.1 500 Internal Server Error");
|
||||
assert!(server.get("one.test", "/").contains("First site"), "server still serving");
|
||||
assert_eq!(server.send("gemini", b"gemini://one.test/abyss\r\n"), "40 Temporary failure\r\n");
|
||||
assert_eq!(server.send("spartan", b"one.test /abyss 0\r\n"), "5 Internal error\r\n");
|
||||
assert!(server.get("one.test", "/").contains("First site"), "still serving after all five");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_redirect_target_cannot_inject_a_response_header() {
|
||||
// The file name contains a real CR LF. Unescaped, the 301's Location line
|
||||
// ends early and the rest becomes a header the server never meant to send.
|
||||
let server = Server::start();
|
||||
let response = server.get("one.test", "/split%0d%0aX-Injected:%20yes.md");
|
||||
assert!(!response.contains("\r\nX-Injected: yes"), "header injected: {response}");
|
||||
assert!(response.contains("Location: /split%0D%0AX-Injected%3A%20yes"), "{response}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_injected_redirect_target_is_escaped_on_every_protocol() {
|
||||
// Spartan and Gemini put the target in a status line terminated the same way,
|
||||
// so the same file name would split their responses too.
|
||||
let server = Server::start();
|
||||
let spartan = server.send("spartan", b"one.test /split%0d%0aX-Injected:%20yes.md 0\r\n");
|
||||
assert_eq!(spartan, "3 /split%0D%0AX-Injected%3A%20yes\r\n");
|
||||
let gemini = server.send("gemini", b"gemini://one.test/split%0d%0aX-Injected:%20yes.md\r\n");
|
||||
assert_eq!(gemini, "31 /split%0D%0AX-Injected%3A%20yes\r\n");
|
||||
}
|
||||
|
||||
// -- Containment ---------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue