]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
v4: Define arguments for %(rest: ) xlat and parse / escape URIs (#4127)
authorNick Porter <nick@portercomputing.co.uk>
Thu, 15 Jul 2021 02:12:22 +0000 (03:12 +0100)
committerGitHub <noreply@github.com>
Thu, 15 Jul 2021 02:12:22 +0000 (21:12 -0500)
* Define args for rest xlat

* Define xlat_uri_part_t - for defining parts of URIs for parsing

* Add xlat_parse_uri() to parse defined URI structures.

* Define url_part_escape() to escape part of a URI

* Define rest_uri_parts

* Update rest_xlat() to parse inbound boxes and escape tainted values

* Update syntax for %(rest: ) tests

* Add tests using inbound data for other parts of a rest URI

* Update documentation for %(rest: )

* Ensure we "exdent" before returning

* Add additional test cases for %(rest: )

* Make sure test web server handles different paths correctly

doc/antora/modules/raddb/pages/mods-available/json.adoc
doc/antora/modules/raddb/pages/mods-available/rest.adoc
raddb/mods-available/json
raddb/mods-available/rest
scripts/ci/openresty/json-api.lua
src/lib/unlang/xlat.h
src/lib/unlang/xlat_eval.c
src/modules/rlm_rest/rlm_rest.c
src/tests/modules/rest/rest_xlat.attrs
src/tests/modules/rest/rest_xlat.unlang

index ff2d7c4d5da90cd4604eaba2b3c35a693ef87b23..45ff62297b8e75388eb313a2904f601f09acc5c6 100644 (file)
@@ -66,7 +66,7 @@ fields can be accessed using 'map' as shown in the example below.
 
 [source, unlang]
 ----
-map json "%{rest:GET http://example.org/api/user/%{User-Name}" {
+map json "%(rest:GET http://example.org/api/user/%{User-Name})" {
   &Tmp-Integer-0  := '$.account number'
   &Group          += '$.groups.*'
 }
index 69fc6fa34d0ef75800200522729ce0b947cffb24..44509794f00bc53f8bc43aedff7cb682e1147370 100644 (file)
@@ -204,7 +204,7 @@ The values of those attributes should be in the format:
   <attribute>: <value>
 
 `control.REST-HTTP-Header` attributes will be consumed after each call
-to the rest module, and each `%{rest:}` expansion.
+to the rest module, and each `%(rest:)` expansion.
 
 
 
@@ -426,6 +426,30 @@ or increase lifetime/idle_timeout.
 ====
 
 
+## xlat for REST calls
+
+An xlat based on the instance name can be used to perform REST calls.
+
+.Example
+
+```
+%(rest:http://www.example.com/)
+%(rest:GET https://www.example.com/user/%{User-Name})
+%(rest:POST https://www.example.com/auth/%{User-Name} %{Called-Station-Id})
+```
+
+The xlat takes up to 3 arguments:
+
+  * Method
+  * URI
+  * Body data
+
+If only one argument is presented, that will be taken as the URI and the
+method will default to GET.
+
+Any attributes in the xlat read from the request packet will be escaped.
+
+
 == Default Configuration
 
 ```
index fa15768f371a0428561ebf8c20f3e1dca8e8181a..c6c2fd884868fd4d7b8ddd8ea5d8a34945d59be5 100644 (file)
@@ -67,7 +67,7 @@
 #
 #  [source, unlang]
 #  ----
-#  map json "%{rest:GET http://example.org/api/user/%{User-Name}" {
+#  map json "%(rest:GET http://example.org/api/user/%{User-Name})" {
 #    &Tmp-Integer-0  := '$.account number'
 #    &Group          += '$.groups.*'
 #  }
index 9613ec7390386ef19dc4a4f8cec95e7b8f575f12..340619f2c87cdb51a74c76db68bb0a474e9cf1d6 100644 (file)
@@ -225,7 +225,7 @@ rest {
        #    <attribute>: <value>
        #
        #  `control.REST-HTTP-Header` attributes will be consumed after each call
-       #  to the rest module, and each `%{rest:}` expansion.
+       #  to the rest module, and each `%(rest:)` expansion.
        #
 
        #
index add2da15a60a12c9e23ef6e855585a847105362e..96a2429ecadb3fa5ebb3f8fbde3c75c9d12dc983 100644 (file)
@@ -64,6 +64,10 @@ function Api.endpoint(method, path, callback)
             -- If chunk contains <something>
             if string.find(k, "<(.-)>")
             then
+                if not splitReqPath[i] then
+                    reqPath = origPath
+                    return false
+                end
                 -- Add to keyData
                 keyData[string.match(k, "%<(%a+)%>")] = splitReqPath[i]
                 -- Replace matches with default for validation
@@ -131,3 +135,11 @@ Api.endpoint('GET', '/user/<username>/mac/<client>',
     end
 )
 
+-- Simple reflection of a URI argument
+Api.endpoint('GET', '/user/<username>/reflect/',
+    function(body, keyData)
+        local returnData = {}
+        returnData["station"] = uriArgs.station
+        return ngx.say(cjson.encode(returnData))
+    end
+)
index b6cdf4c60556471ccbd9269b39c29df72533233b..4b3b3948e1dbe851e03554b4a05b9bcd381f892f 100644 (file)
@@ -126,6 +126,21 @@ typedef struct {
 #define XLAT_ARG_PARSER_TERMINATOR { .required = false, .concat = false, .single = false, .variadic = false, \
                                        .type = FR_TYPE_NULL, .func = NULL, .uctx = NULL }
 
+/** Definition for a single part of a URI
+ *
+ */
+typedef struct {
+       char const              *name;                          //!< Name of this part of the URI
+       fr_sbuff_term_t const   *terminals;                     //!< Characters that mark the end of this part.
+       uint8_t const           part_adv[UINT8_MAX + 1];        //!< How many parts to advance for a specific terminal
+       size_t                  extra_skip;                     //!< How many additional characters to skip after
+                                                               ///< the terminal
+       bool                    tainted_allowed;                //!< Do we accept tainted values for this part
+       xlat_escape_func_t      func;                           //!< Function to use to escape tainted values
+} xlat_uri_part_t;
+
+#define XLAT_URI_PART_TERMINATOR { .name = NULL, .terminals = NULL, .tainted_allowed = false, .func = NULL }
+
 /** A callback when the the timeout occurs
  *
  * Used when a xlat needs wait for an event.
@@ -328,6 +343,8 @@ int         xlat_eval_pair(request_t *request, fr_pair_t *vp);
 
 bool           xlat_async_required(xlat_exp_t const *xlat);
 
+int            xlat_parse_uri(request_t *request, fr_value_box_list_t *uri, xlat_uri_part_t const *uri_parts, void *uctx);
+
 ssize_t                xlat_tokenize_ephemeral(TALLOC_CTX *ctx, xlat_exp_t **head, xlat_flags_t *flags,
                                        fr_sbuff_t *in,
                                        fr_sbuff_parse_rules_t const *p_rules, tmpl_rules_t const *t_rules);
index 284cdbd9914c1dc9a34971bcc5d76a8fe69f4046..357f76363cb1e12536e5d27bc10959b0bccd6202 100644 (file)
@@ -2216,3 +2216,84 @@ bool xlat_async_required(xlat_exp_t const *xlat)
 
        return false;
 }
+
+
+/** Parse a list of value boxes representing a URI
+ *
+ * Reads a URI from a list of value boxes and parses it according to the
+ * definition in uri_parts.  Tainted values, where allowed, are escaped
+ * using the function specified for the uri part.
+ *
+ * @param request      currently being processed
+ * @param uri          to parse
+ * @param uri_parts    definition of URI structure
+ * @param uctx         to pass to escaping function
+ * @return
+ *     - 0 on success
+ *     - -1 on failure
+ */
+int xlat_parse_uri(request_t *request, fr_value_box_list_t *uri, xlat_uri_part_t const *uri_parts, void *uctx)
+{
+       fr_value_box_t          *uri_vb = NULL;
+       xlat_uri_part_t const   *uri_part;
+       fr_sbuff_t              sbuff;
+       char const              *p;
+
+       uri_part = uri_parts;
+
+       while ((uri_vb = fr_dlist_next(uri, uri_vb))){
+               if (uri_vb->tainted && !uri_part->tainted_allowed) {
+                       REDEBUG("Tainted value not allowed for %s", uri_part->name);
+                       return -1;
+               }
+
+               /*
+                *      Tainted boxes can only belong to a single part of the URI
+                */
+               if (uri_vb->tainted) {
+                       if ((uri_part->func) && (uri_part->func(request, uri_vb, uctx) < 0)) {
+                               REDEBUG("Unable to escape tainted input %pV", uri_vb);
+                               return -1;
+                       }
+                       continue;
+               }
+
+               /*
+                *      This URI part has no term chars - so no need to look for them
+                */
+               if (!uri_part->terminals) continue;
+
+               /*
+                *      Zero length box - no terminators here
+                */
+               if (uri_vb->length == 0) continue;
+
+               /*
+                *      Look for URI part terminator
+                */
+               fr_sbuff_init(&sbuff, uri_vb->vb_strvalue, uri_vb->length);
+
+               do {
+                       fr_sbuff_adv_until(&sbuff, SIZE_MAX, uri_part->terminals, '\0');
+                       p = fr_sbuff_current(&sbuff);
+
+                       /*
+                        *      We've not found a terminal in the current box
+                        */
+                       if (uri_part->part_adv[(uint8_t)*p] == 0) continue;
+
+                       /*
+                        *      This terminator has trailing characters to skip
+                        */
+                       if (uri_part->extra_skip) fr_sbuff_advance(&sbuff, uri_part->extra_skip);
+
+                       /*
+                        *      Move to the next part
+                        */
+                       uri_part += uri_part->part_adv[(uint8_t)*p];
+                       if (!uri_part->terminals) break;
+               } while (fr_sbuff_advance(&sbuff, 1) > 0);
+       }
+
+       return 0;
+}
index 215f77d2a5095f12c388440f24069e937c6595d1..20790d98d39dca176284cd87d4cf22212ff5de85 100644 (file)
@@ -169,6 +169,7 @@ static int rlm_rest_status_update(request_t *request, void *handle)
        if (!code) {
                pair_delete_request(attr_rest_http_status_code);
                RDEBUG2("&request.REST-HTTP-Status-Code !* ANY");
+               REXDENT();
                return -1;
        }
 
@@ -292,16 +293,78 @@ finish:
        return xa;
 }
 
+/** URL escape a single box forming part of a URL
+ *
+ * @param request      being processed
+ * @param vb           to escape
+ * @param uctx         context containing CURL handle
+ * @return
+ *     - 0 on success
+ *     - -1 on failure
+ */
+static int uri_part_escape(request_t *request, fr_value_box_t *vb, void *uctx)
+{
+       char                    *escaped;
+       fr_curl_io_request_t    *randle = talloc_get_type_abort(uctx, fr_curl_io_request_t);
+       fr_dlist_t              entry;
+
+       escaped = curl_easy_escape(randle->candle, vb->vb_strvalue, vb->length);
+       if (!escaped) return -1;
+
+       /*
+        *      Returned string the same length - nothing changed
+        */
+       if (strlen(escaped) == vb->length) {
+               RDEBUG4("Tainted value %pV needed no escaping", vb);
+               curl_free(escaped);
+               return 0;
+       }
+
+       RDEBUG4("Tainted value %pV escaped to %s", vb, escaped);
+       /*
+        *      Store list pointers to restore later - fr_value_box_clear() clears them
+        */
+       entry = vb->entry;
+
+       fr_value_box_clear(vb);
+
+       fr_value_box_strdup(vb, vb, NULL, escaped, vb->tainted);
+       vb->entry.next = entry.next;
+       vb->entry.prev = entry.prev;
+
+       curl_free(escaped);
+
+       return 0;
+}
+
+static xlat_uri_part_t const rest_uri_parts[] = {
+       { .name = "scheme", .terminals = &FR_SBUFF_TERMS(L(":")), .part_adv = { [':'] = 1 },
+         .tainted_allowed = false, .extra_skip = 2 },
+       { .name = "host", .terminals = &FR_SBUFF_TERMS(L(":"), L("/")), .part_adv = { [':'] = 1, ['/'] = 2 },
+         .tainted_allowed = true, .func = uri_part_escape },
+       { .name = "port", .terminals = &FR_SBUFF_TERMS(L("/")), .part_adv = { ['/'] = 1 },
+         .tainted_allowed = false },
+       { .name = "method", .terminals = &FR_SBUFF_TERMS(L("?")), .part_adv = { ['?'] = 1 },
+         .tainted_allowed = true, .func = uri_part_escape },
+       { .name = "param", .tainted_allowed = true, .func = uri_part_escape },
+       XLAT_URI_PART_TERMINATOR
+};
+
+static xlat_arg_parser_t const rest_xlat_args[] = {
+       { .required = true, .variadic = true, .type = FR_TYPE_STRING },
+       XLAT_ARG_PARSER_TERMINATOR
+};
+
 /** Simple xlat to read text data from a URL
  *
  * Example:
 @verbatim
-%{rest:http://example.com/}
+%(rest:http://example.com/)
 @endverbatim
  *
  * @ingroup xlat_functions
  */
-static xlat_action_t rest_xlat(TALLOC_CTX *ctx, UNUSED fr_dcursor_t *out,
+static xlat_action_t rest_xlat(UNUSED TALLOC_CTX *ctx, UNUSED fr_dcursor_t *out,
                               request_t *request, UNUSED void const *xlat_inst, void *xlat_thread_inst,
                               fr_value_box_list_t *in)
 {
@@ -310,28 +373,14 @@ static xlat_action_t rest_xlat(TALLOC_CTX *ctx, UNUSED fr_dcursor_t *out,
        rlm_rest_thread_t               *t = xti->t;
 
        fr_curl_io_request_t            *randle = NULL;
-       ssize_t                         len;
        int                             ret;
-       char                            *uri = NULL;
-       char const                      *p = NULL, *q;
        http_method_t                   method;
-       fr_value_box_t                  *in_head = fr_dlist_head(in);
+       fr_value_box_t                  *in_vb = fr_dlist_pop_head(in), *uri_vb = NULL;
 
        /* There are no configurable parameters other than the URI */
        rlm_rest_xlat_rctx_t            *rctx;
        rlm_rest_section_t              *section;
 
-       if (!in_head) {
-               REDEBUG("Got empty URL string");
-               return XLAT_ACTION_FAIL;
-       }
-
-       if (fr_value_box_list_concat(ctx, in_head, in, FR_TYPE_STRING, true) < 0) {
-               REDEBUG("Failed concatenating arguments into URL string");
-               return XLAT_ACTION_FAIL;
-       }
-       p = in_head->vb_strvalue;
-
        MEM(rctx = talloc(request, rlm_rest_xlat_rctx_t));
        section = &rctx->section;
 
@@ -340,48 +389,58 @@ static xlat_action_t rest_xlat(TALLOC_CTX *ctx, UNUSED fr_dcursor_t *out,
         */
        memcpy(&rctx->section, &mod_inst->xlat, sizeof(*section));
 
-       RDEBUG2("Expanding URI components");
+       fr_assert(in_vb->type == FR_TYPE_GROUP);
 
        /*
-        *  Extract the method from the start of the format string (if there is one)
+        *      If we have more than 1 argument, then the first is the method
         */
-       method = fr_table_value_by_substr(http_method_table, p, -1, REST_HTTP_METHOD_UNKNOWN);
-       if (method != REST_HTTP_METHOD_UNKNOWN) {
-               section->method = method;
-               p += http_method_table[method].name.len;
-       /*
-        *  If the method is unknown, it's either a URL or a verb
-        */
-       } else {
-               for (q = p; (*q != ' ') && (*q != '\0') && isalpha(*q); q++);
+       if  ((fr_dlist_head(in))) {
+               uri_vb = fr_dlist_head(&in_vb->vb_group);
+               if (fr_value_box_list_concat(uri_vb, uri_vb, &in_vb->vb_group, FR_TYPE_STRING, true) < 0) {
+                       REDEBUG("Failed concatenating argument");
+                       return XLAT_ACTION_FAIL;
+               }
+               method = fr_table_value_by_substr(http_method_table, uri_vb->vb_strvalue, -1, REST_HTTP_METHOD_UNKNOWN);
 
+               if (method != REST_HTTP_METHOD_UNKNOWN) {
+                       section->method = method;
                /*
-                *      If the first non-alpha char was a space,
-                *      then assume this is a verb.
-                */
-               if ((*q == ' ') && (q != p)) {
-                       section->method = REST_HTTP_METHOD_CUSTOM;
-                       MEM(section->method_str = talloc_bstrndup(rctx, p, q - p));
-                       p = q;
+                *  If the method is unknown, it's a custom verb
+                */
                } else {
-                       section->method = REST_HTTP_METHOD_GET;
+                       section->method = REST_HTTP_METHOD_CUSTOM;
+                       MEM(section->method_str = talloc_bstrndup(rctx, in_vb->vb_strvalue, in_vb->vb_length));
                }
+               /*
+                *      Move to next argument
+                */
+               in_vb = fr_dlist_pop_head(in);
+               uri_vb = NULL;
+       } else {
+               section->method = REST_HTTP_METHOD_GET;
        }
 
        /*
-        *  Trim whitespace
+        *      We get a connection from the pool here as the CURL object
+        *      is needed to use curl_easy_escape() for escaping
         */
-       fr_skip_whitespace(p);
-
        randle = rctx->handle = fr_pool_connection_get(t->pool, request);
        if (!randle) return XLAT_ACTION_FAIL;
 
        /*
-        *  Unescape parts of xlat'd URI, this allows REST servers to be specified by
-        *  request attributes.
+        *      Walk the incomming boxes, assessing where each is in the URI,
+        *      escaping tainted ones where needed.  Following each space in the
+        *      input a new VB group is started.
         */
-       len = rest_uri_host_unescape(&uri, mod_inst, request, randle, p);
-       if (len <= 0) {
+
+       fr_assert(in_vb->type == FR_TYPE_GROUP);
+
+       if (xlat_parse_uri(request, &in_vb->vb_group, rest_uri_parts, randle) < 0) return XLAT_ACTION_FAIL;
+
+       uri_vb = fr_dlist_head(&in_vb->vb_group);
+
+       if (fr_value_box_list_concat(uri_vb, uri_vb, &in_vb->vb_group, FR_TYPE_STRING, true) < 0) {
+               REDEBUG("Failed to concatenate URI");
        error:
                rest_request_cleanup(mod_inst, randle);
                fr_pool_connection_release(t->pool, request, randle);
@@ -391,18 +450,21 @@ static xlat_action_t rest_xlat(TALLOC_CTX *ctx, UNUSED fr_dcursor_t *out,
        }
 
        /*
-        *  Extract freeform body data (url can't contain spaces)
+        *      Any additional arguments are freeform data
         */
-       q = strchr(p, ' ');
-       if (q && (*++q != '\0')) {
+       if ((in_vb = fr_dlist_head(in))) {
+               if (fr_value_box_list_concat(in_vb, in_vb, in, FR_TYPE_STRING, true) < 0) {
+                       REDEBUG("Failed to concatenate freeform data");
+                       goto error;
+               }
                section->body = REST_HTTP_BODY_CUSTOM_LITERAL;
-               section->data = q;
+               section->data = in_vb->vb_strvalue;
        }
 
-       RDEBUG2("Sending HTTP %s to \"%s\"",
+       RDEBUG2("Sending HTTP %s to \"%pV\"",
               (section->method == REST_HTTP_METHOD_CUSTOM) ?
                section->method_str : fr_table_str_by_value(http_method_table, section->method, NULL),
-              uri);
+              uri_vb);
 
        /*
         *  Configure various CURL options, and initialise the read/write
@@ -410,9 +472,8 @@ static xlat_action_t rest_xlat(TALLOC_CTX *ctx, UNUSED fr_dcursor_t *out,
         *
         *  @todo We could extract the User-Name and password from the URL string.
         */
-       ret = rest_request_config(mod_inst, t, section, request,
-                                 randle, section->method, section->body, uri, NULL, NULL);
-       talloc_free(uri);
+       ret = rest_request_config(mod_inst, t, section, request, randle, section->method,
+                                 section->body, uri_vb->vb_strvalue, NULL, NULL);
        if (ret < 0) goto error;
 
        /*
@@ -1116,13 +1177,14 @@ static int mod_instantiate(void *instance, CONF_SECTION *conf)
 
 static int mod_bootstrap(void *instance, CONF_SECTION *conf)
 {
-       rlm_rest_t *inst = instance;
-       xlat_t const *xlat;
+       rlm_rest_t      *inst = instance;
+       xlat_t          *xlat;
 
        inst->xlat_name = cf_section_name2(conf);
        if (!inst->xlat_name) inst->xlat_name = cf_section_name1(conf);
 
        xlat = xlat_register(inst, inst->xlat_name, rest_xlat, true);
+       xlat_func_args(xlat, rest_xlat_args);
        xlat_async_thread_instantiate_set(xlat, mod_xlat_thread_instantiate, rest_xlat_thread_inst_t, NULL, inst);
 
        return 0;
index 14c4cc8ec6454573eaa3a9393d93cb040c9a1486..ac7da710bdb22523a9369aa6af2d651329946ce9 100644 (file)
@@ -6,6 +6,10 @@ User-Name = 'Bob'
 User-Password = 'Saget'
 Called-Station-Id = 'aa:bb:cc:dd:ee:ff'
 NAS-IP-Address = '192.168.1.1'
+Login-IP-Host = 127.0.0.1
+NAS-Port = 8080
+Calling-Station-Id = 'dummy&unsafe=escaped'
+Tmp-String-9 = ''
 
 #
 #  Expected answer
index 30a6ff95b840a3134ba9bacdf37069a067e67302..d455f13f7c9d404bee7ba7beb0c3f3cd790a15c8 100644 (file)
@@ -11,7 +11,7 @@ update request {
 
 # Retrieve a plain text file
 update control {
-       &Tmp-String-1 := "%{rest:GET http://%{Tmp-String-0}:%{Tmp-Integer-0}/test.txt}"
+       &Tmp-String-1 := "%(rest:GET http://%{Tmp-String-0}:%{Tmp-Integer-0}/test.txt)"
 }
 
 if (&REST-HTTP-Status-Code != 200) {
@@ -22,9 +22,27 @@ if (&control.Tmp-String-1 != "Sample text response\n") {
        test_fail
 }
 
+# Take host from incomming packet
+update control {
+       &Tmp-String-1 := "%(rest:http://%{Login-IP-Host}:%{Tmp-Integer-0}/test.txt)"
+}
+
+if ((&REST-HTTP-Status-Code != 200) || (&control.Tmp-String-1 != "Sample text response\n")) {
+       test_fail
+}
+
+# Port is not allowed from incomming packet
+update control {
+       &Tmp-String-1 := "%(rest:http://%{Tmp-String-0}:%{NAS-Port}/test.txt)"
+}
+
+if ((&Module-Failure-Message != "Tainted value not allowed for port") || (&control.Tmp-String-1 != "")) {
+       test_fail
+}
+
 # Check a "not found" gives a 404 status code
 update control {
-       &Tmp-String-1 := "%{rest:GET http://%{Tmp-String-0}:%{Tmp-Integer-0}/%{Tmp-String-1}}"
+       &Tmp-String-1 := "%(rest:GET http://%{Tmp-String-0}:%{Tmp-Integer-0}/%{Tmp-String-1})"
 }
 
 if (&REST-HTTP-Status-Code != 404) {
@@ -33,7 +51,7 @@ if (&REST-HTTP-Status-Code != 404) {
 
 # GET with URL parameters
 update request {
-       &Tmp-String-2 := "%{rest:GET http://%{Tmp-String-0}:%{Tmp-Integer-0}/user/%{User-Name}/mac/%{Called-Station-Id}}"
+       &Tmp-String-2 := "%(rest:GET http://%{Tmp-String-0}:%{Tmp-Integer-0}/user/%{User-Name}/mac/%{Called-Station-Id})"
 }
 
 if (&REST-HTTP-Status-Code != 200) {
@@ -58,7 +76,7 @@ update control {
 }
 
 # Directly use json map and prepend the returned value
-map json "%{rest:GET http://%{Tmp-String-0}:%{Tmp-Integer-0}/user/%{User-Name}/mac/%{Called-Station-Id}}" {
+map json "%(rest:GET http://%{Tmp-String-0}:%{Tmp-Integer-0}/user/%{User-Name}/mac/%{Called-Station-Id})" {
        &control.Tmp-String-3 ^= '$.control\.User-Name.value'
 }
 
@@ -72,7 +90,7 @@ update control {
 
 # POST to https with JSON body data
 update request {
-       &Tmp-String-2 := "%{rest:POST https://%{Tmp-String-0}:%{Tmp-Integer-1}/user/%{User-Name}/mac/%{Called-Station-Id}?section=accounting %{control.Tmp-String-2}}"
+       &Tmp-String-2 := "%(rest:POST https://%{Tmp-String-0}:%{Tmp-Integer-1}/user/%{User-Name}/mac/%{Called-Station-Id}?section=accounting %{control.Tmp-String-2})"
 }
 
 if (&REST-HTTP-Status-Code != 200) {
@@ -103,7 +121,7 @@ update control {
 
 # POST to https with POST body data
 update request {
-       &Tmp-String-2 := "%{rest:POST https://%{Tmp-String-0}:%{Tmp-Integer-1}/post/test?section=dummy %{control.Tmp-String-2}}"
+       &Tmp-String-2 := "%(rest:POST https://%{Tmp-String-0}:%{Tmp-Integer-1}/post/test?section=dummy %{control.Tmp-String-2})"
 }
 
 if (&REST-HTTP-Status-Code != 200) {
@@ -114,4 +132,36 @@ if (&Tmp-String-2 != "Section: dummy, User: Bob\n") {
        test_fail
 }
 
+# URI with tainted values in the arguments - input argument includes URI argument
+# separator - make sure this doesn't end up generating extra arguments, but gets escaped.
+update request {
+       &Tmp-String-2 := "%(rest:GET http://%{Tmp-String-0}:%{Tmp-Integer-0}/user/%{User-Name}/reflect/?station=%{Calling-Station-Id})"
+}
+
+if (&Tmp-String-2 != "{\"station\":\"dummy&unsafe=escaped\"}\n" ) {
+       test_fail
+}
+
+# Zero length untainted value - check parsing doesn't break on zero length string
+update request {
+       &Tmp-String-8 := ""
+}
+
+update request {
+       &Tmp-String-2 := "%(rest:http://%{Tmp-String-0}:%{Tmp-Integer-0}/user/%{User-Name}/reflect/%{Tmp-String-8}?station=%{User-Name})"
+}
+
+if (&Tmp-String-2 != "{\"station\":\"Bob\"}\n" ) {
+       test_fail
+}
+
+# Zero length tainted value - check escaping doesn't break on zero length string
+update request {
+       &Tmp-String-2 := "%(rest:http://%{Tmp-String-0}:%{Tmp-Integer-0}/user/%{User-Name}/reflect/%{Tmp-String-9}?station=%{Called-Station-Id})"
+}
+
+if (&Tmp-String-2 != "{\"station\":\"aa:bb:cc:dd:ee:ff\"}\n" ) {
+       test_fail
+}
+
 test_pass