aboutsummaryrefslogtreecommitdiffstats
path: root/tests/create_directories.rs
blob: 380c796bed07e3f52cb68d2947aff90c96917066 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
mod fixtures;

use fixtures::{server, Error, TestServer, DIRECTORIES};
use reqwest::blocking::{multipart, Client};
use rstest::rstest;
use select::document::Document;
use select::predicate::{Attr, Text};
#[cfg(unix)]
use std::os::unix::fs::symlink as symlink_dir;
#[cfg(windows)]
use std::os::windows::fs::symlink_dir;

/// This should work because the flags for uploading files and creating directories
/// are set, and the directory name and path are valid.
#[rstest]
fn creating_directories_works(
    #[with(&["--upload-files", "--mkdir"])] server: TestServer,
) -> Result<(), Error> {
    let test_directory_name = "hello";

    // Before creating, check whether the directory does not yet exist.
    let body = reqwest::blocking::get(server.url())?.error_for_status()?;
    let parsed = Document::from_read(body)?;
    assert!(parsed.find(Text).all(|x| x.text() != test_directory_name));

    // Perform the actual creation.
    let create_action = parsed
        .find(Attr("id", "mkdir"))
        .next()
        .expect("Couldn't find element with id=mkdir")
        .attr("action")
        .expect("Directory form doesn't have action attribute");
    let form = multipart::Form::new();
    let part = multipart::Part::text(test_directory_name);
    let form = form.part("mkdir", part);

    let client = Client::new();
    client
        .post(server.url().join(create_action)?)
        .multipart(form)
        .send()?
        .error_for_status()?;

    // After creating, check whether the directory is now getting listed.
    let body = reqwest::blocking::get(server.url())?;
    let parsed = Document::from_read(body)?;
    assert!(parsed
        .find(Text)
        .any(|x| x.text() == test_directory_name.to_owned() + "/"));

    Ok(())
}

/// This should fail because the server does not allow for creating directories
/// as the flags for uploading files and creating directories are not set.
/// The directory name and path are valid.
#[rstest]
fn creating_directories_is_prevented(server: TestServer) -> Result<(), Error> {
    let test_directory_name = "hello";

    // Before creating, check whether the directory does not yet exist.
    let body = reqwest::blocking::get(server.url())?.error_for_status()?;
    let parsed = Document::from_read(body)?;
    assert!(parsed.find(Text).all(|x| x.text() != test_directory_name));

    // Ensure the directory creation form is not present
    assert!(parsed.find(Attr("id", "mkdir")).next().is_none());

    // Then try to create anyway
    let form = multipart::Form::new();
    let part = multipart::Part::text(test_directory_name);
    let form = form.part("mkdir", part);

    let client = Client::new();
    // This should fail
    assert!(client
        .post(server.url().join("/upload?path=/")?)
        .multipart(form)
        .send()?
        .error_for_status()
        .is_err());

    // After creating, check whether the directory is now getting listed (shouldn't).
    let body = reqwest::blocking::get(server.url())?;
    let parsed = Document::from_read(body)?;
    assert!(parsed
        .find(Text)
        .all(|x| x.text() != test_directory_name.to_owned() + "/"));

    Ok(())
}

/// This should fail because directory creation through symlinks should not be possible
/// when the the no symlinks flag is set.
#[rstest]
fn creating_directories_through_symlinks_is_prevented(
    #[with(&["--upload-files", "--mkdir", "--no-symlinks"])] server: TestServer,
) -> Result<(), Error> {
    // Make symlinks
    let symlink_directory_str = "symlink";
    let symlink_directory = server.path().join(symlink_directory_str);
    let symlinked_direcotry = server.path().join(DIRECTORIES[0]);
    symlink_dir(symlinked_direcotry, symlink_directory).unwrap();

    // Before attempting to create, ensure the symlink does not exist.
    let body = reqwest::blocking::get(server.url())?.error_for_status()?;
    let parsed = Document::from_read(body)?;
    assert!(parsed.find(Text).all(|x| x.text() != symlink_directory_str));

    // Attempt to perform directory creation.
    let form = multipart::Form::new();
    let part = multipart::Part::text(symlink_directory_str);
    let form = form.part("mkdir", part);

    // This should fail
    assert!(Client::new()
        .post(
            server
                .url()
                .join(format!("/upload?path=/{symlink_directory_str}").as_str())?
        )
        .multipart(form)
        .send()?
        .error_for_status()
        .is_err());

    Ok(())
}

/// Test for path traversal vulnerability (CWE-22) in both path parameter of query string and in
/// mkdir name (Content-Disposition)
///
/// see: https://github.com/svenstaro/miniserve/issues/518
#[rstest]
#[case("foo", "bar", "foo/bar")] // Not CWE-22, but `foo` isn't a directory
#[case("/../foo", "bar", "foo/bar")]
#[case("/foo", "/../bar", "foo/bar")]
#[case("C:/foo", "C:/bar", if cfg!(windows) { "foo/bar" } else { "C:/foo/C:/bar" })]
#[case(r"C:\foo", r"C:\bar", if cfg!(windows) { "foo/bar" } else { r"C:\foo/C:\bar" })]
#[case(r"\foo", r"\..\bar", if cfg!(windows) { "foo/bar" } else { r"\foo/\..\bar" })]
fn prevent_path_transversal_attacks(
    #[with(&["--upload-files", "--mkdir"])] server: TestServer,
    #[case] path: &str,
    #[case] dir_name: &'static str,
    #[case] expected: &str,
) -> Result<(), Error> {
    let expected_path = server.path().join(expected);
    assert!(!expected_path.exists());

    let form = multipart::Form::new();
    let part = multipart::Part::text(dir_name);
    let form = form.part("mkdir", part);

    // This should fail
    assert!(Client::new()
        .post(server.url().join(&format!("/upload/path={path}"))?)
        .multipart(form)
        .send()?
        .error_for_status()
        .is_err());

    Ok(())
}