]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
add file handling functions
authorAlan T. DeKok <aland@freeradius.org>
Mon, 16 Oct 2023 12:20:49 +0000 (08:20 -0400)
committerAlan T. DeKok <aland@freeradius.org>
Mon, 16 Oct 2023 12:26:26 +0000 (08:26 -0400)
doc/antora/modules/reference/nav.adoc
doc/antora/modules/reference/pages/xlat/file.adoc [new file with mode: 0644]
src/lib/unlang/xlat_builtin.c

index c4cf6061ca49f5e02480fa8265beb6fb3b98a4d8..0b9f90843bb15cb2a17b63c48664373305acfc14 100644 (file)
@@ -63,7 +63,8 @@
 
 ** xref:xlat/index.adoc[Dynamic Expansion]
 *** xref:xlat/alternation.adoc[Alternation Syntax]
-*** xref:xlat/function.adoc[Functions]
+*** xref:xlat/file.adoc[File handling]
+*** xref:xlat/function.adoc[Function Syntax]
 *** xref:xlat/builtin.adoc[Built-in Expansions]
 *** xref:xlat/character.adoc[Single Letter Expansions]
 *** xref:xlat/attribute.adoc[Attribute References]
diff --git a/doc/antora/modules/reference/pages/xlat/file.adoc b/doc/antora/modules/reference/pages/xlat/file.adoc
new file mode 100644 (file)
index 0000000..8da93f8
--- /dev/null
@@ -0,0 +1,73 @@
+= File Handling functions
+
+The file handling functions allow for a limited number of operations on files.
+
+The filenames can be taken from `tainted` sources, in which cases special characters such as '`/`' and '`$`' are escaped.  Any special character is replaced with an underscore, followed by the hex value of the character.  Valid UTF-8 characters are allowed.
+
+For example, the `tainted` string `user@freeradius.org/..` will turn into the filename `user@freeradius.org_2f..`.  This operation renders the filename "safe" for operations on the local file system.  It is not possible for `tainted` data to create files, or to perform directory traversal attacks.
+
+== %file.exists(_string_)
+
+.Return: _bool_
+
+This function returns `true` if a file exists, or `false` if the file does not exist.
+
+.Checking if a file exists
+====
+[source,unlang]
+----
+if %file.exists("/var/log/radius.log") {
+       # do things...
+}
+----
+====
+
+== %file.head(_string_)
+
+.Return: _string_
+
+This function returns the first line of the file.  If the file does not exist, or if the line is more than 256 characters in length, it fails and nothing is returned.
+
+.Returning the first line of a file
+====
+[source,unlang]
+----
+string line
+
+&line := %file.head("/var/log/radius.log")
+----
+====
+
+== %file.size(_string_)
+
+.Return: _uint64_
+
+This function checks the size of a file.
+
+.Checking the size of a file
+====
+[source,unlang]
+----
+if (%file.size("/var/log/radius.log") > (((uint64)1) << 20)) {
+       %log.info("The log file is getting large!")
+}
+----
+
+== %file.rm(_string_)
+
+.Return: _bool_
+
+This function removes a file.  If the file exists and could be removed, it returns `true`.  Otherwise if the file does not exist, or if the file could not be removed, it returns `false.
+
+.Removing a file
+====
+[source,unlang]
+----
+if (%file.size("/var/log/radius.log") > (((uint64)1) << 20)) {
+       %file.rm("/var/log/radius.log")
+}
+----
+====
+
+// Copyright (C) 2023 Network RADIUS SAS.  Licenced under CC-by-NC 4.0.
+// This documentation was developed by Network RADIUS SAS.
index f6867fb70b7f9a140a901bfe657c0ea2cfdbdfbd..867fca9ffec2adb4a4dc65f7e46e0429d28891b2 100644 (file)
@@ -330,6 +330,22 @@ static const fr_sbuff_escape_rules_t xlat_filename_escape = {
        .do_utf8 = true,
        .do_hex = true,
 
+       .esc = {
+               [ 0x00 ... 0x2d ] = true,               // special characters, but not '.'
+               [ 0x2f ] = true,                        // /
+               [ 0x3A ... 0x3f ] = true,               // :;<=>?, but not "@"
+               [ 0x5b ... 0x5e ] = true,               // [\]^
+               [ 0x60 ] = true,                        // back-tick
+               [ 0x7b ... 0xff ] = true,               // {|}, and all chars which have high bit set, but aren't UTF-8
+       },
+};
+
+static const fr_sbuff_escape_rules_t xlat_filename_escape_dots = {
+       .name = "filename",
+       .chr = '_',
+       .do_utf8 = true,
+       .do_hex = true,
+
        .esc = {
                [ 0x00 ... 0x2f ] = true,               // special characters, '.', '/', etc.
                [ 0x3A ... 0x3f ] = true,               // :;<=>?, but not "@"
@@ -342,54 +358,74 @@ static const fr_sbuff_escape_rules_t xlat_filename_escape = {
 /** Escape the paths as necessary
  *
  */
-static ssize_t xlat_file_escape_path(fr_sbuff_t *in, fr_value_box_list_t *list)
+static ssize_t xlat_file_escape_path(fr_sbuff_t *in, fr_value_box_t *vb)
 {
        fr_sbuff_t our_in = FR_SBUFF(in);
        char buffer[256];
 
-       fr_value_box_list_foreach(list, vb) {
-               if (vb->type == FR_TYPE_GROUP) {
-                       if (xlat_file_escape_path(&our_in, &vb->vb_group) < 0) return -1;
-                       continue;
+       if (vb->type == FR_TYPE_GROUP) {
+               fr_value_box_list_foreach(&vb->vb_group, box) {
+                       if (xlat_file_escape_path(&our_in, box) < 0) return -1;
                }
 
-               fr_assert(fr_type_is_leaf(vb->type));
+               goto done;
+       }
 
-               /*
-                *      Untainted values get passed through, as do
-                *      base integer types.
-                */
-               if (!vb->tainted || (vb->type == FR_TYPE_OCTETS) || fr_type_is_integer(vb->type)) {
-                       fr_value_box_print(&our_in, vb, NULL);
-                       continue;
-               }
+       fr_assert(fr_type_is_leaf(vb->type));
 
-               if (vb->type == FR_TYPE_STRING) {
+       /*
+        *      Untainted values get passed through, as do base integer types.
+        */
+       if (!vb->tainted || (vb->type == FR_TYPE_OCTETS) || fr_type_is_integer(vb->type)) {
+               fr_value_box_print(&our_in, vb, NULL);
+               goto done;
+       }
+
+       /*
+        *      If the tainted string has a leading '.', then escape _all_ periods in it.  This is so that we
+        *      don't accidentally allow a "safe" value to end with '/', and then an "unsafe" value contains
+        *      "..", and we now have a directory traversal attack.
+        *
+        *      The escape rules will escape '/' in unsafe strings, so there's no possibility for an unsafe
+        *      string to either end with a '/', or to contain "/.." itself.
+        *
+        *      Allowing '.' in the middle of the string means we can have filenames based on realms, such as
+        *      "log/aland@freeradius.org".
+        */
+       if (vb->type == FR_TYPE_STRING) {
+               if (vb->vb_length == 0) goto done;
+
+               if (vb->vb_strvalue[0] == '.') {
+                       fr_value_box_print(&our_in, vb, &xlat_filename_escape_dots);
+               } else {
                        fr_value_box_print(&our_in, vb, &xlat_filename_escape);
-                       continue;
                }
-
-               /*
-                *      Ethernet addresses have ':'.  IP prefixes have '/'.  Floats have '+' and '-' in them.
-                *      Dates have spaces.
-                *
-                *      We use an intermediate buffer to print the type, and then copy it to the output
-                *      buffer, escaping it along the way.
-                */
-               fr_value_box_print(&FR_SBUFF_OUT(buffer, sizeof(buffer)), vb, NULL);
-               fr_sbuff_in_escape(&our_in, buffer, strlen(buffer), &xlat_filename_escape);
+               goto  done;
        }
 
+       /*
+        *      Ethernet addresses have ':'.  IP prefixes have '/'.  Floats have '+' and '-' in them.
+        *      Dates have pretty much all of that, plus spaces.
+        *
+        *      Lesson: print dates as %Y() or %l().
+        *
+        *      We use an intermediate buffer to print the type, and then copy it to the output
+        *      buffer, escaping it along the way.
+        */
+       fr_value_box_print(&FR_SBUFF_OUT(buffer, sizeof(buffer)), vb, NULL);
+       fr_sbuff_in_escape(&our_in, buffer, strlen(buffer), &xlat_filename_escape);
+
+done:
        FR_SBUFF_SET_RETURN(in, &our_in);
 }
 
-static const char *xlat_file_name(fr_value_box_list_t *in)
+static const char *xlat_file_name(fr_value_box_t *vb)
 {
        fr_sbuff_t      *path;
 
        FR_SBUFF_TALLOC_THREAD_LOCAL(&path, 256, PATH_MAX + 1);
 
-       if (xlat_file_escape_path(path, in) < 0) return NULL;
+       if (xlat_file_escape_path(path, vb) < 0) return NULL;
 
        if (fr_sbuff_in_char(path, '\0') < 0) return NULL; /* file functions take NUL delimited strings */
 
@@ -403,13 +439,14 @@ static xlat_arg_parser_t const xlat_func_file_exists_args[] = {
 
 static xlat_action_t xlat_func_file_exists(TALLOC_CTX *ctx, fr_dcursor_t *out,
                                           UNUSED xlat_ctx_t const *xctx,
-                                          UNUSED request_t *request, fr_value_box_list_t *in)
+                                          UNUSED request_t *request, fr_value_box_list_t *args)
 {
-       fr_value_box_t *dst;
+       fr_value_box_t *dst, *vb;
        char const      *filename;
        struct stat     buf;
 
-       filename = xlat_file_name(in);
+       XLAT_ARGS(args, &vb);
+       filename = xlat_file_name(vb);
        if (!filename) return XLAT_ACTION_FAIL;
 
        MEM(dst = fr_value_box_alloc(ctx, FR_TYPE_BOOL, NULL));
@@ -425,13 +462,14 @@ static xlat_action_t xlat_func_file_head(TALLOC_CTX *ctx, fr_dcursor_t *out,
                                         UNUSED xlat_ctx_t const *xctx,
                                         request_t *request, fr_value_box_list_t *in)
 {
-       fr_value_box_t *dst;
+       fr_value_box_t *dst, *vb;
        char const      *filename;
        ssize_t         len;
        int             fd;
        char            *p, buffer[256];
 
-       filename = xlat_file_name(in);
+       XLAT_ARGS(in, &vb);
+       filename = xlat_file_name(vb);
        if (!filename) return XLAT_ACTION_FAIL;
 
        fd = open(filename, O_RDONLY);
@@ -482,11 +520,12 @@ static xlat_action_t xlat_func_file_size(TALLOC_CTX *ctx, fr_dcursor_t *out,
                                           UNUSED xlat_ctx_t const *xctx,
                                           request_t *request, fr_value_box_list_t *in)
 {
-       fr_value_box_t *dst;
+       fr_value_box_t *dst, *vb;
        char const      *filename;
        struct stat     buf;
 
-       filename = xlat_file_name(in);
+       XLAT_ARGS(in, &vb);
+       filename = xlat_file_name(vb);
        if (!filename) return XLAT_ACTION_FAIL;
 
        if (stat(filename, &buf) < 0) {
@@ -507,10 +546,11 @@ static xlat_action_t xlat_func_file_rm(TALLOC_CTX *ctx, fr_dcursor_t *out,
                                           UNUSED xlat_ctx_t const *xctx,
                                           request_t *request, fr_value_box_list_t *in)
 {
-       fr_value_box_t *dst;
+       fr_value_box_t *dst, *vb;
        char const      *filename;
 
-       filename = xlat_file_name(in);
+       XLAT_ARGS(in, &vb);
+       filename = xlat_file_name(vb);
        if (!filename) return XLAT_ACTION_FAIL;
 
        MEM(dst = fr_value_box_alloc(ctx, FR_TYPE_BOOL, NULL));
@@ -570,7 +610,7 @@ static xlat_arg_parser_t const xlat_func_explode_args[] = {
 update request {
        &Tmp-String-1 := "a,b,c"
 }
-"%concat(%explode(%{Tmp-String-1}, ','), '|')" == "a|b|c"
+"%concat(%explode(%{Tmp-String-1}, ','), '|')" == "a|b|c"g
 @endverbatim
  *
  * @ingroup xlat_functions