Seregon/zftpd

Zero-copy FTP/HTTP Daemon compatible with all POSIX systems

C/11.0 KB/No license
tests/test_http_confinement.c
zftpd / tests / test_http_confinement.c
1/**
2 * @file test_http_confinement.c
3 * @brief Regression tests for HTTP root path confinement
4 *
5 * Tests http_validate_and_confine() indirectly via http_api_set_root()
6 * and the validate_path() pipeline. Covers:
7 *
8 * TEST 1: Legitimate path inside root -> OK
9 * TEST 2: Traversal via /../ -> REJECTED
10 * TEST 3: Double-slash traversal (//../..) -> REJECTED
11 * TEST 4: Symlink escaping root -> REJECTED
12 * TEST 5: Absolute path outside root -> REJECTED
13 * TEST 6: Root "/" permits everything -> OK
14 * TEST 7: http_csrf_init returns int -> VERIFIED
15 *
16 * Exit codes:
17 * 0 = all tests passed
18 * N = test N failed
19 * 99 = setup failure
20 */
21 
22#include "ftp_path.h"
23#include "http_api.h"
24#include "http_csrf.h"
25#include <stdio.h>
26#include <stdlib.h>
27#include <string.h>
28#include <sys/stat.h>
29#include <unistd.h>
30 
31/*===========================================================================*
32 * HELPERS
33 *===========================================================================*/
34 
35#define FAIL(n, msg) \
36 do { \
37 fprintf(stderr, "FAIL test %d: %s\n", (n), (msg)); \
38 goto cleanup; \
39 } while (0)
40 
41#define PASS(n) \
42 do { \
43 fprintf(stderr, " PASS test %d\n", (n)); \
44 } while (0)
45 
46/**
47 * @brief Test if a path is confined to root using the same pipeline
48 * as http_api.c: ftp_path_normalize -> ftp_path_is_within_root
49 *
50 * @return 0 if confined, -1 if escapes root
51 */
52static int test_confine(const char *input, const char *root, char *out,
53 size_t out_size) {
54 char normalized[FTP_PATH_MAX];
55 if (ftp_path_normalize(input, normalized, sizeof(normalized)) != FTP_OK) {
56 return -1;
57 }
58 if (ftp_path_is_within_root(normalized, root) != 1) {
59 return -1;
60 }
61 
62 /* Resolve symlinks if possible */
63 char real[FTP_PATH_MAX];
64 if (realpath(normalized, real) != NULL) {
65 if (ftp_path_is_within_root(real, root) != 1) {
66 return -1;
67 }
68 size_t n = strlen(real);
69 if ((n + 1U) > out_size) {
70 return -1;
71 }
72 memcpy(out, real, n + 1U);
73 } else {
74 size_t n = strlen(normalized);
75 if ((n + 1U) > out_size) {
76 return -1;
77 }
78 memcpy(out, normalized, n + 1U);
79 }
80 return 0;
81}
82 
83/*===========================================================================*
84 * MAIN
85 *===========================================================================*/
86 
87int main(void) {
88 int result = 99;
89 
90 /*-- Setup: create temp directory tree --*/
91 char tmpl[] = "/tmp/zhttp-test-XXXXXX";
92 char *base = mkdtemp(tmpl);
93 if (base == NULL) {
94 fprintf(stderr, "mkdtemp failed\n");
95 return 99;
96 }
97 
98 char root[FTP_PATH_MAX];
99 (void)snprintf(root, sizeof(root), "%s/root", base);
100 if (mkdir(root, 0700) != 0) {
101 return 99;
102 }
103 
104 char sub[FTP_PATH_MAX];
105 (void)snprintf(sub, sizeof(sub), "%s/files", root);
106 if (mkdir(sub, 0700) != 0) {
107 return 99;
108 }
109 
110 /* Create a symlink inside root that points outside */
111 char linkp[FTP_PATH_MAX];
112 (void)snprintf(linkp, sizeof(linkp), "%s/escape", root);
113 (void)symlink("/tmp", linkp);
114 
115 /* Resolve the real root path (macOS /tmp -> /private/tmp) */
116 char root_real[FTP_PATH_MAX];
117 if (realpath(root, root_real) == NULL) {
118 return 99;
119 }
120 
121 /* Set the HTTP API root */
122 http_api_set_root(root_real);
123 
124 char out[FTP_PATH_MAX];
125 
126 /*-- TEST 1: Legitimate path inside root --*/
127 {
128 char path[FTP_PATH_MAX];
129 (void)snprintf(path, sizeof(path), "%s/files", root_real);
130 if (test_confine(path, root_real, out, sizeof(out)) != 0) {
131 FAIL(1, "legitimate path rejected");
132 }
133 PASS(1);
134 }
135 
136 /*-- TEST 2: Traversal via /../ --*/
137 {
138 char path[FTP_PATH_MAX];
139 (void)snprintf(path, sizeof(path), "%s/../..", root_real);
140 if (test_confine(path, root_real, out, sizeof(out)) == 0) {
141 FAIL(2, "traversal path accepted");
142 }
143 PASS(2);
144 }
145 
146 /*-- TEST 3: Double-slash traversal --*/
147 {
148 char path[FTP_PATH_MAX];
149 (void)snprintf(path, sizeof(path), "%s///../..", root_real);
150 if (test_confine(path, root_real, out, sizeof(out)) == 0) {
151 FAIL(3, "double-slash traversal accepted");
152 }
153 PASS(3);
154 }
155 
156 /*-- TEST 4: Symlink escaping root --*/
157 {
158 char path[FTP_PATH_MAX];
159 (void)snprintf(path, sizeof(path), "%s/escape", root_real);
160 if (test_confine(path, root_real, out, sizeof(out)) == 0) {
161 FAIL(4, "symlink escape accepted");
162 }
163 PASS(4);
164 }
165 
166 /*-- TEST 5: Absolute path outside root --*/
167 {
168 if (test_confine("/etc/passwd", root_real, out, sizeof(out)) == 0) {
169 FAIL(5, "absolute outside path accepted");
170 }
171 PASS(5);
172 }
173 
174 /*-- TEST 6: Root "/" permits everything --*/
175 {
176 if (test_confine("/etc/passwd", "/", out, sizeof(out)) != 0) {
177 FAIL(6, "root '/' rejected a valid path");
178 }
179 PASS(6);
180 }
181 
182 /*-- TEST 7: CSRF init returns int --*/
183 {
184 /*
185 * http_csrf_init() should return 0 on success (we have
186 * /dev/urandom on macOS/Linux) or -1 on failure.
187 * Either way, it must return an int, not void.
188 */
189 int csrf_rc = http_csrf_init();
190 if (csrf_rc != 0 && csrf_rc != -1) {
191 FAIL(7, "csrf_init returned unexpected value");
192 }
193 PASS(7);
194 }
195 
196 fprintf(stderr, "[HTTP CONFINEMENT] All tests passed\n");
197 result = 0;
198 
199cleanup:
200 (void)unlink(linkp);
201 (void)rmdir(sub);
202 (void)rmdir(root);
203 (void)rmdir(base);
204 return result;
205}
206