Add fuzzing of multipart_parser
[project/cgi-io.git] / main.c
1 /*
2 * cgi-io - LuCI non-RPC helper
3 *
4 * Copyright (C) 2013 Jo-Philipp Wich <jo@mein.io>
5 *
6 * Permission to use, copy, modify, and/or distribute this software for any
7 * purpose with or without fee is hereby granted, provided that the above
8 * copyright notice and this permission notice appear in all copies.
9 *
10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 */
18
19 #define _GNU_SOURCE /* splice(), SPLICE_F_MORE */
20
21 #include <stdio.h>
22 #include <stdlib.h>
23 #include <stdbool.h>
24 #include <unistd.h>
25 #include <string.h>
26 #include <errno.h>
27 #include <fcntl.h>
28 #include <ctype.h>
29 #include <sys/stat.h>
30 #include <sys/wait.h>
31 #include <sys/sendfile.h>
32 #include <sys/ioctl.h>
33 #include <linux/fs.h>
34
35 #include <libubus.h>
36 #include <libubox/blobmsg.h>
37
38 #include "util.h"
39 #include "multipart_parser.h"
40
41 #ifndef O_TMPFILE
42 #define O_TMPFILE (020000000 | O_DIRECTORY)
43 #endif
44
45 #define READ_BLOCK 4096
46
47 enum part {
48 PART_UNKNOWN,
49 PART_SESSIONID,
50 PART_FILENAME,
51 PART_FILEMODE,
52 PART_FILEDATA
53 };
54
55 const char *parts[] = {
56 "(bug)",
57 "sessionid",
58 "filename",
59 "filemode",
60 "filedata",
61 };
62
63 struct state
64 {
65 bool is_content_disposition;
66 enum part parttype;
67 char *sessionid;
68 char *filename;
69 bool filedata;
70 int filemode;
71 int filefd;
72 int tempfd;
73 };
74
75 enum {
76 SES_ACCESS,
77 __SES_MAX,
78 };
79
80 static const struct blobmsg_policy ses_policy[__SES_MAX] = {
81 [SES_ACCESS] = { .name = "access", .type = BLOBMSG_TYPE_BOOL },
82 };
83
84
85 static struct state st;
86
87 static void
88 session_access_cb(struct ubus_request *req, int type, struct blob_attr *msg)
89 {
90 struct blob_attr *tb[__SES_MAX];
91 bool *allow = (bool *)req->priv;
92
93 if (!msg)
94 return;
95
96 blobmsg_parse(ses_policy, __SES_MAX, tb, blob_data(msg), blob_len(msg));
97
98 if (tb[SES_ACCESS])
99 *allow = blobmsg_get_bool(tb[SES_ACCESS]);
100 }
101
102 static bool
103 session_access(const char *sid, const char *scope, const char *obj, const char *func)
104 {
105 uint32_t id;
106 bool allow = false;
107 struct ubus_context *ctx;
108 static struct blob_buf req;
109
110 ctx = ubus_connect(NULL);
111
112 if (!ctx || !obj || ubus_lookup_id(ctx, "session", &id))
113 goto out;
114
115 blob_buf_init(&req, 0);
116 blobmsg_add_string(&req, "ubus_rpc_session", sid);
117 blobmsg_add_string(&req, "scope", scope);
118 blobmsg_add_string(&req, "object", obj);
119 blobmsg_add_string(&req, "function", func);
120
121 ubus_invoke(ctx, id, "access", req.head, session_access_cb, &allow, 500);
122
123 out:
124 if (ctx)
125 ubus_free(ctx);
126
127 return allow;
128 }
129
130 static char *
131 checksum(const char *applet, size_t sumlen, const char *file)
132 {
133 pid_t pid;
134 int r;
135 int fds[2];
136 static char chksum[65];
137
138 if (pipe(fds))
139 return NULL;
140
141 switch ((pid = fork()))
142 {
143 case -1:
144 return NULL;
145
146 case 0:
147 uloop_done();
148
149 dup2(fds[1], 1);
150
151 close(0);
152 close(2);
153 close(fds[0]);
154 close(fds[1]);
155
156 if (execl("/bin/busybox", "/bin/busybox", applet, file, NULL))
157 return NULL;
158
159 break;
160
161 default:
162 memset(chksum, 0, sizeof(chksum));
163 r = read(fds[0], chksum, sumlen);
164
165 waitpid(pid, NULL, 0);
166 close(fds[0]);
167 close(fds[1]);
168
169 if (r < 0)
170 return NULL;
171 }
172
173 return chksum;
174 }
175
176 static int
177 response(bool success, const char *message)
178 {
179 char *chksum;
180 struct stat s;
181
182 printf("Status: 200 OK\r\n");
183 printf("Content-Type: text/plain\r\n\r\n{\n");
184
185 if (success)
186 {
187 if (!stat(st.filename, &s))
188 printf("\t\"size\": %u,\n", (unsigned int)s.st_size);
189 else
190 printf("\t\"size\": null,\n");
191
192 chksum = checksum("md5sum", 32, st.filename);
193 printf("\t\"checksum\": %s%s%s,\n",
194 chksum ? "\"" : "",
195 chksum ? chksum : "null",
196 chksum ? "\"" : "");
197
198 chksum = checksum("sha256sum", 64, st.filename);
199 printf("\t\"sha256sum\": %s%s%s\n",
200 chksum ? "\"" : "",
201 chksum ? chksum : "null",
202 chksum ? "\"" : "");
203 }
204 else
205 {
206 if (message)
207 printf("\t\"message\": \"%s\",\n", message);
208
209 printf("\t\"failure\": [ %u, \"%s\" ]\n", errno, strerror(errno));
210
211 if (st.filefd > -1 && st.filename)
212 unlink(st.filename);
213 }
214
215 printf("}\n");
216
217 return -1;
218 }
219
220 static int
221 failure(int code, int e, const char *message)
222 {
223 printf("Status: %d %s\r\n", code, message);
224 printf("Content-Type: text/plain\r\n\r\n");
225 printf("%s", message);
226
227 if (e)
228 printf(": %s", strerror(e));
229
230 printf("\n");
231
232 return -1;
233 }
234
235 static int
236 filecopy(void)
237 {
238 int len;
239 char buf[READ_BLOCK];
240
241 if (!st.filedata)
242 {
243 close(st.tempfd);
244 errno = EINVAL;
245 return response(false, "No file data received");
246 }
247
248 snprintf(buf, sizeof(buf), "/proc/self/fd/%d", st.tempfd);
249
250 if (unlink(st.filename) < 0 && errno != ENOENT)
251 {
252 close(st.tempfd);
253 return response(false, "Failed to unlink existing file");
254 }
255
256 if (linkat(AT_FDCWD, buf, AT_FDCWD, st.filename, AT_SYMLINK_FOLLOW) < 0)
257 {
258 if (lseek(st.tempfd, 0, SEEK_SET) < 0)
259 {
260 close(st.tempfd);
261 return response(false, "Failed to rewind temp file");
262 }
263
264 st.filefd = open(st.filename, O_CREAT | O_TRUNC | O_WRONLY, 0600);
265
266 if (st.filefd < 0)
267 {
268 close(st.tempfd);
269 return response(false, "Failed to open target file");
270 }
271
272 while ((len = read(st.tempfd, buf, sizeof(buf))) > 0)
273 {
274 if (write(st.filefd, buf, len) != len)
275 {
276 close(st.tempfd);
277 close(st.filefd);
278 return response(false, "I/O failure while writing target file");
279 }
280 }
281
282 close(st.filefd);
283 }
284
285 close(st.tempfd);
286
287 if (chmod(st.filename, st.filemode))
288 return response(false, "Failed to chmod target file");
289
290 return 0;
291 }
292
293 static int
294 header_field(multipart_parser *p, const char *data, size_t len)
295 {
296 st.is_content_disposition = !strncasecmp(data, "Content-Disposition", len);
297 return 0;
298 }
299
300 static int
301 header_value(multipart_parser *p, const char *data, size_t len)
302 {
303 size_t i, j;
304
305 if (!st.is_content_disposition)
306 return 0;
307
308 if (len < 10 || strncasecmp(data, "form-data", 9))
309 return 0;
310
311 for (data += 9, len -= 9; *data == ' ' || *data == ';'; data++, len--);
312
313 if (len < 8 || strncasecmp(data, "name=\"", 6))
314 return 0;
315
316 for (data += 6, len -= 6, i = 0; i <= len; i++)
317 {
318 if (*(data + i) != '"')
319 continue;
320
321 for (j = 1; j < sizeof(parts) / sizeof(parts[0]); j++)
322 if (!strncmp(data, parts[j], i))
323 st.parttype = j;
324
325 break;
326 }
327
328 return 0;
329 }
330
331 static int
332 data_begin_cb(multipart_parser *p)
333 {
334 if (st.parttype == PART_FILEDATA)
335 {
336 if (!st.sessionid)
337 return response(false, "File data without session");
338
339 if (!st.filename)
340 return response(false, "File data without name");
341
342 if (!session_access(st.sessionid, "file", st.filename, "write"))
343 return response(false, "Access to path denied by ACL");
344
345 st.tempfd = open("/tmp", O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR);
346
347 if (st.tempfd < 0)
348 return response(false, "Failed to create temporary file");
349 }
350
351 return 0;
352 }
353
354 static int
355 data_cb(multipart_parser *p, const char *data, size_t len)
356 {
357 int wlen = len;
358
359 switch (st.parttype)
360 {
361 case PART_SESSIONID:
362 st.sessionid = datadup(data, len);
363 break;
364
365 case PART_FILENAME:
366 st.filename = canonicalize_path(data, len);
367 break;
368
369 case PART_FILEMODE:
370 st.filemode = strtoul(data, NULL, 8);
371 break;
372
373 case PART_FILEDATA:
374 if (write(st.tempfd, data, len) != wlen)
375 {
376 close(st.tempfd);
377 return response(false, "I/O failure while writing temporary file");
378 }
379
380 if (!st.filedata)
381 st.filedata = !!wlen;
382
383 break;
384
385 default:
386 break;
387 }
388
389 return 0;
390 }
391
392 static int
393 data_end_cb(multipart_parser *p)
394 {
395 if (st.parttype == PART_SESSIONID)
396 {
397 if (!session_access(st.sessionid, "cgi-io", "upload", "write"))
398 {
399 errno = EPERM;
400 return response(false, "Upload permission denied");
401 }
402 }
403 else if (st.parttype == PART_FILEDATA)
404 {
405 if (st.tempfd < 0)
406 return response(false, "Internal program failure");
407
408 #if 0
409 /* prepare directory */
410 for (ptr = st.filename; *ptr; ptr++)
411 {
412 if (*ptr == '/')
413 {
414 *ptr = 0;
415
416 if (mkdir(st.filename, 0755))
417 {
418 unlink(st.tmpname);
419 return response(false, "Failed to create destination directory");
420 }
421
422 *ptr = '/';
423 }
424 }
425 #endif
426
427 if (filecopy())
428 return -1;
429
430 return response(true, NULL);
431 }
432
433 st.parttype = PART_UNKNOWN;
434 return 0;
435 }
436
437 static multipart_parser *
438 init_parser(void)
439 {
440 char *boundary;
441 const char *var;
442
443 multipart_parser *p;
444 static multipart_parser_settings s = {
445 .on_part_data = data_cb,
446 .on_headers_complete = data_begin_cb,
447 .on_part_data_end = data_end_cb,
448 .on_header_field = header_field,
449 .on_header_value = header_value
450 };
451
452 var = getenv("CONTENT_TYPE");
453
454 if (!var || strncmp(var, "multipart/form-data;", 20))
455 return NULL;
456
457 for (var += 20; *var && *var != '='; var++);
458
459 if (*var++ != '=')
460 return NULL;
461
462 boundary = malloc(strlen(var) + 3);
463
464 if (!boundary)
465 return NULL;
466
467 strcpy(boundary, "--");
468 strcpy(boundary + 2, var);
469
470 st.tempfd = -1;
471 st.filefd = -1;
472 st.filemode = 0600;
473
474 p = multipart_parser_init(boundary, &s);
475
476 free(boundary);
477
478 return p;
479 }
480
481 static int
482 main_upload(int argc, char *argv[])
483 {
484 int rem, len;
485 bool done = false;
486 char buf[READ_BLOCK];
487 multipart_parser *p;
488
489 p = init_parser();
490
491 if (!p)
492 {
493 errno = EINVAL;
494 return response(false, "Invalid request");
495 }
496
497 while ((len = read(0, buf, sizeof(buf))) > 0)
498 {
499 if (!done) {
500 rem = multipart_parser_execute(p, buf, len);
501 done = (rem < len);
502 }
503 }
504
505 multipart_parser_free(p);
506
507 return 0;
508 }
509
510 static void
511 free_charp(char **ptr)
512 {
513 free(*ptr);
514 }
515
516 #define autochar __attribute__((__cleanup__(free_charp))) char
517
518 static int
519 main_download(int argc, char **argv)
520 {
521 char *fields[] = { "sessionid", NULL, "path", NULL, "filename", NULL, "mimetype", NULL };
522 unsigned long long size = 0;
523 char *p, buf[READ_BLOCK];
524 ssize_t len = 0;
525 struct stat s;
526 int rfd;
527
528 autochar *post = postdecode(fields, 4);
529 (void) post;
530
531 if (!fields[1] || !session_access(fields[1], "cgi-io", "download", "read"))
532 return failure(403, 0, "Download permission denied");
533
534 if (!fields[3] || !session_access(fields[1], "file", fields[3], "read"))
535 return failure(403, 0, "Access to path denied by ACL");
536
537 if (stat(fields[3], &s))
538 return failure(404, errno, "Failed to stat requested path");
539
540 if (!S_ISREG(s.st_mode) && !S_ISBLK(s.st_mode))
541 return failure(403, 0, "Requested path is not a regular file or block device");
542
543 for (p = fields[5]; p && *p; p++)
544 if (!isalnum(*p) && !strchr(" ()<>@,;:[]?.=%-", *p))
545 return failure(400, 0, "Invalid characters in filename");
546
547 for (p = fields[7]; p && *p; p++)
548 if (!isalnum(*p) && !strchr(" .;=/-", *p))
549 return failure(400, 0, "Invalid characters in mimetype");
550
551 rfd = open(fields[3], O_RDONLY);
552
553 if (rfd < 0)
554 return failure(500, errno, "Failed to open requested path");
555
556 if (S_ISBLK(s.st_mode))
557 ioctl(rfd, BLKGETSIZE64, &size);
558 else
559 size = (unsigned long long)s.st_size;
560
561 printf("Status: 200 OK\r\n");
562 printf("Content-Type: %s\r\n", fields[7] ? fields[7] : "application/octet-stream");
563
564 if (fields[5])
565 printf("Content-Disposition: attachment; filename=\"%s\"\r\n", fields[5]);
566
567 if (size > 0) {
568 printf("Content-Length: %llu\r\n\r\n", size);
569 fflush(stdout);
570
571 while (size > 0) {
572 len = sendfile(1, rfd, NULL, size);
573
574 if (len == -1) {
575 if (errno == ENOSYS || errno == EINVAL) {
576 while ((len = read(rfd, buf, sizeof(buf))) > 0)
577 fwrite(buf, len, 1, stdout);
578
579 fflush(stdout);
580 break;
581 }
582
583 if (errno == EINTR || errno == EAGAIN)
584 continue;
585 }
586
587 if (len <= 0)
588 break;
589
590 size -= len;
591 }
592 }
593 else {
594 printf("\r\n");
595
596 while ((len = read(rfd, buf, sizeof(buf))) > 0)
597 fwrite(buf, len, 1, stdout);
598
599 fflush(stdout);
600 }
601
602 close(rfd);
603
604 return 0;
605 }
606
607 static int
608 main_backup(int argc, char **argv)
609 {
610 pid_t pid;
611 time_t now;
612 int r;
613 int len;
614 int status;
615 int fds[2];
616 char datestr[16] = { 0 };
617 char hostname[64] = { 0 };
618 char *fields[] = { "sessionid", NULL };
619
620 autochar *post = postdecode(fields, 1);
621 (void) post;
622
623 if (!fields[1] || !session_access(fields[1], "cgi-io", "backup", "read"))
624 return failure(403, 0, "Backup permission denied");
625
626 if (pipe(fds))
627 return failure(500, errno, "Failed to spawn pipe");
628
629 switch ((pid = fork()))
630 {
631 case -1:
632 return failure(500, errno, "Failed to fork process");
633
634 case 0:
635 dup2(fds[1], 1);
636
637 close(0);
638 close(2);
639 close(fds[0]);
640 close(fds[1]);
641
642 r = chdir("/");
643 if (r < 0)
644 return failure(500, errno, "Failed chdir('/')");
645
646 execl("/sbin/sysupgrade", "/sbin/sysupgrade",
647 "--create-backup", "-", NULL);
648
649 return -1;
650
651 default:
652 close(fds[1]);
653
654 now = time(NULL);
655 strftime(datestr, sizeof(datestr) - 1, "%Y-%m-%d", localtime(&now));
656
657 if (gethostname(hostname, sizeof(hostname) - 1))
658 sprintf(hostname, "OpenWrt");
659
660 printf("Status: 200 OK\r\n");
661 printf("Content-Type: application/x-targz\r\n");
662 printf("Content-Disposition: attachment; "
663 "filename=\"backup-%s-%s.tar.gz\"\r\n\r\n", hostname, datestr);
664
665 fflush(stdout);
666
667 do {
668 len = splice(fds[0], NULL, 1, NULL, READ_BLOCK, SPLICE_F_MORE);
669 } while (len > 0);
670
671 waitpid(pid, &status, 0);
672
673 close(fds[0]);
674
675 return 0;
676 }
677 }
678
679
680 static const char *
681 lookup_executable(const char *cmd)
682 {
683 size_t plen = 0, clen;
684 static char path[PATH_MAX];
685 char *search, *p;
686 struct stat s;
687
688 if (!cmd)
689 return NULL;
690
691 clen = strlen(cmd) + 1;
692
693 if (!stat(cmd, &s) && S_ISREG(s.st_mode))
694 return cmd;
695
696 search = getenv("PATH");
697
698 if (!search)
699 search = "/bin:/usr/bin:/sbin:/usr/sbin";
700
701 p = search;
702
703 do {
704 if (*p != ':' && *p != '\0')
705 continue;
706
707 plen = p - search;
708
709 if ((plen + clen) >= sizeof(path))
710 continue;
711
712 strncpy(path, search, plen);
713 sprintf(path + plen, "/%s", cmd);
714
715 if (!stat(path, &s) && S_ISREG(s.st_mode))
716 return path;
717
718 search = p + 1;
719 } while (*p++);
720
721 return NULL;
722 }
723
724 static int
725 main_exec(int argc, char **argv)
726 {
727 char *fields[] = { "sessionid", NULL, "command", NULL, "filename", NULL, "mimetype", NULL };
728 int i, devnull, status, fds[2];
729 bool allowed = false;
730 ssize_t len = 0;
731 const char *exe;
732 char *p, **args;
733 pid_t pid;
734
735 autochar *post = postdecode(fields, 4);
736 (void) post;
737
738 if (!fields[1] || !session_access(fields[1], "cgi-io", "exec", "read"))
739 return failure(403, 0, "Exec permission denied");
740
741 for (p = fields[5]; p && *p; p++)
742 if (!isalnum(*p) && !strchr(" ()<>@,;:[]?.=%-", *p))
743 return failure(400, 0, "Invalid characters in filename");
744
745 for (p = fields[7]; p && *p; p++)
746 if (!isalnum(*p) && !strchr(" .;=/-", *p))
747 return failure(400, 0, "Invalid characters in mimetype");
748
749 args = fields[3] ? parse_command(fields[3]) : NULL;
750
751 if (!args)
752 return failure(400, 0, "Invalid command parameter");
753
754 /* First check if we find an ACL match for the whole cmdline ... */
755 allowed = session_access(fields[1], "file", args[0], "exec");
756
757 /* Now split the command vector... */
758 for (i = 1; args[i]; i++)
759 args[i][-1] = 0;
760
761 /* Find executable... */
762 exe = lookup_executable(args[0]);
763
764 if (!exe) {
765 free(args);
766 return failure(404, 0, "Executable not found");
767 }
768
769 /* If there was no ACL match, check for a match on the executable */
770 if (!allowed && !session_access(fields[1], "file", exe, "exec")) {
771 free(args);
772 return failure(403, 0, "Access to command denied by ACL");
773 }
774
775 if (pipe(fds)) {
776 free(args);
777 return failure(500, errno, "Failed to spawn pipe");
778 }
779
780 switch ((pid = fork()))
781 {
782 case -1:
783 free(args);
784 close(fds[0]);
785 close(fds[1]);
786 return failure(500, errno, "Failed to fork process");
787
788 case 0:
789 devnull = open("/dev/null", O_RDWR);
790
791 if (devnull > -1) {
792 dup2(devnull, 0);
793 dup2(devnull, 2);
794 close(devnull);
795 }
796 else {
797 close(0);
798 close(2);
799 }
800
801 dup2(fds[1], 1);
802 close(fds[0]);
803 close(fds[1]);
804
805 if (chdir("/") < 0) {
806 free(args);
807 return failure(500, errno, "Failed chdir('/')");
808 }
809
810 if (execv(exe, args) < 0) {
811 free(args);
812 return failure(500, errno, "Failed execv(...)");
813 }
814
815 return -1;
816
817 default:
818 close(fds[1]);
819
820 printf("Status: 200 OK\r\n");
821 printf("Content-Type: %s\r\n",
822 fields[7] ? fields[7] : "application/octet-stream");
823
824 if (fields[5])
825 printf("Content-Disposition: attachment; filename=\"%s\"\r\n",
826 fields[5]);
827
828 printf("\r\n");
829 fflush(stdout);
830
831 do {
832 len = splice(fds[0], NULL, 1, NULL, READ_BLOCK, SPLICE_F_MORE);
833 } while (len > 0);
834
835 waitpid(pid, &status, 0);
836
837 close(fds[0]);
838 free(args);
839
840 return 0;
841 }
842 }
843
844 int main(int argc, char **argv)
845 {
846 if (strstr(argv[0], "cgi-upload"))
847 return main_upload(argc, argv);
848 else if (strstr(argv[0], "cgi-download"))
849 return main_download(argc, argv);
850 else if (strstr(argv[0], "cgi-backup"))
851 return main_backup(argc, argv);
852 else if (strstr(argv[0], "cgi-exec"))
853 return main_exec(argc, argv);
854
855 return -1;
856 }