cgi-io: require whitelisting upload locations
authorJo-Philipp Wich <jo@mein.io>
Fri, 30 Aug 2019 05:50:43 +0000 (07:50 +0200)
committerJohn Crispin <john@phrozen.org>
Fri, 30 Aug 2019 11:58:50 +0000 (13:58 +0200)
Introduce further ACL checks to verify that the request-supplied
upload location may be written to. This prevents overwriting things
like /bin/busybox and allows to confine uploads to specific directories.

To setup the required ACLs, the following ubus command may be used
on the command line:

ubus call session grant '{
  "ubus_rpc_session": "d41d8cd98f00b204e9800998ecf8427e",
  "scope": "cgi-io",
  "objects": [
    [ "/etc/certificates/*", "write" ],
    [ "/var/uploads/*", "write" ]
  ]
}'

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
net/cgi-io/Makefile
net/cgi-io/src/main.c

index 2a734b5e58562d895c03725714c994cd932c31d9..5fff39f85daa93107e29fb4771c21ce227c5e135 100644 (file)
@@ -8,7 +8,7 @@
 include $(TOPDIR)/rules.mk
 
 PKG_NAME:=cgi-io
-PKG_RELEASE:=6
+PKG_RELEASE:=7
 
 PKG_LICENSE:=GPL-2.0+
 
index 2bfec623b00d84b37defb4bccfcc08ff95008924..a6ded065f426d81362c953cf893319edf9368b17 100644 (file)
@@ -263,6 +263,64 @@ postdecode(char **fields, int n_fields)
        return (found >= n_fields);
 }
 
+static char *
+canonicalize_path(const char *path, size_t len)
+{
+       char *canonpath, *cp;
+       const char *p, *e;
+
+       if (path == NULL || *path == '\0')
+               return NULL;
+
+       canonpath = datadup(path, len);
+
+       if (canonpath == NULL)
+               return NULL;
+
+       /* normalize */
+       for (cp = canonpath, p = path, e = path + len; p < e; ) {
+               if (*p != '/')
+                       goto next;
+
+               /* skip repeating / */
+               if ((p + 1 < e) && (p[1] == '/')) {
+                       p++;
+                       continue;
+               }
+
+               /* /./ or /../ */
+               if ((p + 1 < e) && (p[1] == '.')) {
+                       /* skip /./ */
+                       if ((p + 2 >= e) || (p[2] == '/')) {
+                               p += 2;
+                               continue;
+                       }
+
+                       /* collapse /x/../ */
+                       if ((p + 2 < e) && (p[2] == '.') && ((p + 3 >= e) || (p[3] == '/'))) {
+                               while ((cp > canonpath) && (*--cp != '/'))
+                                       ;
+
+                               p += 3;
+                               continue;
+                       }
+               }
+
+next:
+               *cp++ = *p++;
+       }
+
+       /* remove trailing slash if not root / */
+       if ((cp > canonpath + 1) && (cp[-1] == '/'))
+               cp--;
+       else if (cp == canonpath)
+               *cp++ = '/';
+
+       *cp = '\0';
+
+       return canonpath;
+}
+
 static int
 response(bool success, const char *message)
 {
@@ -417,6 +475,9 @@ data_begin_cb(multipart_parser *p)
                if (!st.filename)
                        return response(false, "File data without name");
 
+               if (!session_access(st.sessionid, st.filename, "write"))
+                       return response(false, "Access to path denied by ACL");
+
                st.tempfd = mkstemp(tmpname);
 
                if (st.tempfd < 0)
@@ -438,7 +499,7 @@ data_cb(multipart_parser *p, const char *data, size_t len)
                break;
 
        case PART_FILENAME:
-               st.filename = datadup(data, len);
+               st.filename = canonicalize_path(data, len);
                break;
 
        case PART_FILEMODE: