Re: hardening mod_write and mod_proxy like mod_jk with servletnormalize

2020-06-11 Thread Yann Ylavic
On Thu, Jun 11, 2020 at 1:22 PM Yann Ylavic  wrote:
>
> On Thu, Jun 11, 2020 at 9:57 AM Yann Ylavic  wrote:
> >
> > On Thu, Jun 11, 2020 at 9:50 AM Yann Ylavic  wrote:
> > >
> > > We need a way to forward non %-decoded URLs upto mod_proxy (reverse)
> > > if we want to normalize a second time..
> >
> > IOW, this block in ap_process_request_internal():
> [snip]
> > Should go _after_ the following:
> [snip]
>
> Or we could introduce a new pre_translate_name hook which would
> execute before %-decoding, and be used by mod_proxy when
> "ProxyPreTranslation on" is configured, and be a prerequisite for
> mapping=servlet.
>
> I find ProxyPreTranslation also useful for the non-servlet case btw.
>
> Something like this attached v2 patch.

Here is a v3 with the relevant pre_translate_name hooks only and
ap_getparents() preserved when the URI does not start with '/' (which
makes the patch read better too).

>
> Regards;
> Yann.
Index: include/http_request.h
===
--- include/http_request.h	(revision 1878328)
+++ include/http_request.h	(working copy)
@@ -365,6 +365,16 @@ AP_DECLARE_HOOK(int,create_request,(request_rec *r
 
 /**
  * This hook allow modules an opportunity to translate the URI into an
+ * actual filename, before URL decoding happens.
+ * rules will be followed.
+ * @param r The current request
+ * @return OK, DECLINED, or HTTP_...
+ * @ingroup hooks
+ */
+AP_DECLARE_HOOK(int,pre_translate_name,(request_rec *r))
+
+/**
+ * This hook allow modules an opportunity to translate the URI into an
  * actual filename.  If no modules do anything special, the server's default
  * rules will be followed.
  * @param r The current request
Index: include/httpd.h
===
--- include/httpd.h	(revision 1878328)
+++ include/httpd.h	(working copy)
@@ -1779,8 +1779,21 @@ AP_DECLARE(void) ap_no2slash(char *name)
 AP_DECLARE(void) ap_no2slash_ex(char *name, int is_fs_path)
  AP_FN_ATTR_NONNULL_ALL;
 
+#define AP_NORMALIZE_NOT_ABOVE_ROOT (1u <<  0)
+#define AP_NORMALIZE_MERGE_SLASHES  (1u <<  1)
+#define AP_NORMALIZE_PATH_PARAMETERS(1u <<  2)
 
 /**
+ * Remove all , /./ and /xx/../ substrings from a path, and more
+ * depending on passed in flags.
+ * @param path The path to normalize
+ * @param flags bitmask of AP_NORMALIZE_* flags
+ * @return non-zero on success
+ */
+AP_DECLARE(int) ap_normalize_path(char *path, unsigned int flags)
+AP_FN_ATTR_NONNULL((1));
+
+/**
  * Remove all ./ and xx/../ substrings from a file name. Also remove
  * any leading ../ or /../ substrings.
  * @param name the file name to parse
Index: modules/dav/main/util.c
===
--- modules/dav/main/util.c	(revision 1878328)
+++ modules/dav/main/util.c	(working copy)
@@ -664,7 +664,11 @@ static dav_error * dav_process_if_header(request_r
 /* note that parsed_uri.path is allocated; we can trash it */
 
 /* clean up the URI a bit */
-ap_getparents(parsed_uri.path);
+if (!ap_normalize_path(parsed_uri.path, 0)) {
+return dav_new_error(r->pool, HTTP_BAD_REQUEST,
+ DAV_ERR_IF_TAGGED, rv,
+ "Invalid URI path tagged If-header.");
+}
 
 /* the resources we will compare to have unencoded paths */
 if (ap_unescape_url(parsed_uri.path) != OK) {
Index: modules/generators/mod_autoindex.c
===
--- modules/generators/mod_autoindex.c	(revision 1878328)
+++ modules/generators/mod_autoindex.c	(working copy)
@@ -1266,8 +1266,7 @@ static struct ent *make_parent_entry(apr_int32_t a
 if (!(p->name = ap_make_full_path(r->pool, r->uri, "../"))) {
 return (NULL);
 }
-ap_getparents(p->name);
-if (!*p->name) {
+if (!ap_normalize_path(p->name, 0)) {
 return (NULL);
 }
 
Index: modules/generators/mod_info.c
===
--- modules/generators/mod_info.c	(revision 1878328)
+++ modules/generators/mod_info.c	(working copy)
@@ -322,6 +322,7 @@ static const hook_lookup_t request_hooks[] = {
 {"HTTP Scheme", ap_hook_get_http_scheme},
 {"Default Port", ap_hook_get_default_port},
 {"Quick Handler", ap_hook_get_quick_handler},
+{"Pre-Translate Name", ap_hook_get_pre_translate_name},
 {"Translate Name", ap_hook_get_translate_name},
 {"Map to Storage", ap_hook_get_map_to_storage},
 {"Check Access", ap_hook_get_access_checker_ex},
Index: modules/proxy/mod_proxy.c
===
--- modules/proxy/mod_proxy.c	(revision 1878328)
+++ modules/proxy/mod_proxy.c	(working copy)
@@ -753,6 +753,26 @@ PROXY_DECLARE(int) ap_proxy_trans_match(request_re
 mismatch = 

Re: hardening mod_write and mod_proxy like mod_jk with servletnormalize

2020-06-11 Thread Yann Ylavic
On Thu, Jun 11, 2020 at 9:57 AM Yann Ylavic  wrote:
>
> On Thu, Jun 11, 2020 at 9:50 AM Yann Ylavic  wrote:
> >
> > We need a way to forward non %-decoded URLs upto mod_proxy (reverse)
> > if we want to normalize a second time..
>
> IOW, this block in ap_process_request_internal():
[snip]
> Should go _after_ the following:
[snip]

Or we could introduce a new pre_translate_name hook which would
execute before %-decoding, and be used by mod_proxy when
"ProxyPreTranslation on" is configured, and be a prerequisite for
mapping=servlet.

I find ProxyPreTranslation also useful for the non-servlet case btw.

Something like this attached v2 patch.

Regards;
Yann.
Index: include/http_request.h
===
--- include/http_request.h	(revision 1878328)
+++ include/http_request.h	(working copy)
@@ -365,6 +365,16 @@ AP_DECLARE_HOOK(int,create_request,(request_rec *r
 
 /**
  * This hook allow modules an opportunity to translate the URI into an
+ * actual filename, before URL decoding happens.
+ * rules will be followed.
+ * @param r The current request
+ * @return OK, DECLINED, or HTTP_...
+ * @ingroup hooks
+ */
+AP_DECLARE_HOOK(int,pre_translate_name,(request_rec *r))
+
+/**
+ * This hook allow modules an opportunity to translate the URI into an
  * actual filename.  If no modules do anything special, the server's default
  * rules will be followed.
  * @param r The current request
Index: include/httpd.h
===
--- include/httpd.h	(revision 1878328)
+++ include/httpd.h	(working copy)
@@ -1779,8 +1779,21 @@ AP_DECLARE(void) ap_no2slash(char *name)
 AP_DECLARE(void) ap_no2slash_ex(char *name, int is_fs_path)
  AP_FN_ATTR_NONNULL_ALL;
 
+#define AP_NORMALIZE_NOT_ABOVE_ROOT (1u <<  0)
+#define AP_NORMALIZE_MERGE_SLASHES  (1u <<  1)
+#define AP_NORMALIZE_PATH_PARAMETERS(1u <<  2)
 
 /**
+ * Remove all , /./ and /xx/../ substrings from a path, and more
+ * depending on passed in flags.
+ * @param path The path to normalize
+ * @param flags bitmask of AP_NORMALIZE_* flags
+ * @return non-zero on success
+ */
+AP_DECLARE(int) ap_normalize_path(char *path, unsigned int flags)
+AP_FN_ATTR_NONNULL((1));
+
+/**
  * Remove all ./ and xx/../ substrings from a file name. Also remove
  * any leading ../ or /../ substrings.
  * @param name the file name to parse
Index: modules/dav/main/util.c
===
--- modules/dav/main/util.c	(revision 1878328)
+++ modules/dav/main/util.c	(working copy)
@@ -664,7 +664,11 @@ static dav_error * dav_process_if_header(request_r
 /* note that parsed_uri.path is allocated; we can trash it */
 
 /* clean up the URI a bit */
-ap_getparents(parsed_uri.path);
+if (!ap_normalize_path(parsed_uri.path, 0)) {
+return dav_new_error(r->pool, HTTP_BAD_REQUEST,
+ DAV_ERR_IF_TAGGED, rv,
+ "Invalid URI path tagged If-header.");
+}
 
 /* the resources we will compare to have unencoded paths */
 if (ap_unescape_url(parsed_uri.path) != OK) {
Index: modules/examples/mod_example_hooks.c
===
--- modules/examples/mod_example_hooks.c	(revision 1878328)
+++ modules/examples/mod_example_hooks.c	(working copy)
@@ -1176,6 +1176,22 @@ static int x_post_read_request(request_rec *r)
 
 /*
  * This routine gives our module an opportunity to translate the URI into an
+ * actual filename, before URL decoding happens.
+ *
+ * This is a RUN_FIRST hook.
+ */
+static int x_pre_translate_name(request_rec *r)
+{
+/*
+ * We don't actually *do* anything here, except note the fact that we were
+ * called.
+ */
+trace_request(r, "x_pre_translate_name()");
+return DECLINED;
+}
+
+/*
+ * This routine gives our module an opportunity to translate the URI into an
  * actual filename.  If we don't do anything special, the server's default
  * rules (Alias directives and the like) will continue to be followed.
  *
@@ -1467,6 +1483,7 @@ static void x_register_hooks(apr_pool_t *p)
 ap_hook_log_transaction(x_log_transaction, NULL, NULL, APR_HOOK_MIDDLE);
 ap_hook_http_scheme(x_http_scheme, NULL, NULL, APR_HOOK_MIDDLE);
 ap_hook_default_port(x_default_port, NULL, NULL, APR_HOOK_MIDDLE);
+ap_hook_pre_translate_name(x_pre_translate_name, NULL, NULL, APR_HOOK_MIDDLE);
 ap_hook_translate_name(x_translate_name, NULL, NULL, APR_HOOK_MIDDLE);
 ap_hook_map_to_storage(x_map_to_storage, NULL,NULL, APR_HOOK_MIDDLE);
 ap_hook_header_parser(x_header_parser, NULL, NULL, APR_HOOK_MIDDLE);
Index: modules/generators/mod_autoindex.c
===
--- modules/generators/mod_autoindex.c	(revision 1878328)
+++ modules/gen

Re: hardening mod_write and mod_proxy like mod_jk with servletnormalize

2020-06-11 Thread Mark Thomas
On 11/06/2020 07:51, jean-frederic clere wrote:
> On 10/06/2020 11:53, Ruediger Pluem wrote:
>>
>>
>> On 6/9/20 12:05 PM, jean-frederic clere wrote:
>>> Hi,
>>>
>>> Basically it adds servletnormalizecheck to mod_proxy for
>>> ProxyPass/ProxyPassMatch and mod_rewrite when using P
>>> I have tested the following uses:
>>> #ProxyPass  /docs ajp://localhost:8009/docs secret=%A1b2!@
>>> servletnormalizecheck
>>>
>>> #ProxyPassMatch  "^/docs(.*)$" "ajp://localhost:8009/docs$1"
>>> secret=%A1b2!@ servletnormalizecheck
>>>
>>> #RewriteEngine On
>>> #RewriteRule "^/docs(.*)$" "ajp://localhost:8009/docs$1" [P,SNC]
>>> #
>>> #ProxySet connectiontimeout=5 timeout=30 secret=%A1b2!@
>>> #
>>>
>>> #
>>> #  ProxyPass  ajp://localhost:8009/docs secret=%A1b2!@
>>> servletnormalizecheck
>>> #
>>>
>>> What is not supported is
>>> curl -v --path-as-is
>>> "http://localhost:8000/docs/..;foo=bar/;foo=bar/test/index.jsp";
>>>
>>> that could be remapped to
>>> ProxyPass  /test ajp://localhost:8009/test secret=%A1b2!@
>>> servletnormalizecheck
>>> or a 
>>>
>>> Comments?
>>
>> I understood from Mark that the request you do above with curl should
>> not be denied but just mapped to /test.
>> But rethinking that, it becomes real fun: For mapping we should use
>> the URI stripped off path parameters and then having done the
>> shrinking operation (servlet normalized) but we should use the
>> original URI having done the shrinking operation with path
>> parameters to sent to the backend. That might work for a simple prefix
>> matching, but it seems to be very difficult for regular
>> expression scenarios where you might use complex captures from the
>> matching to build the result. But if the matching was done
>> against the servlet normalized URI the captures might be different,
>> than the ones you would have got when doing the same against
>> not normalized URI. So I am little bit lost here.

I can see how this gets complicated for regular expression scenarios.

Since the servlet specification doesn't have the concept of regular
expression mapping, I don't think the rationale for servletnormalize
applies in that case. There is no expectation of how the mapping will
occur from a servlet perspective so the httpd behaviour cannot be
unexpected.

Coming from a servlet perspective I have no view on what the 'correct'
behaviour is in this case. I'll happily support whatever the httpd
community thinks is best.

>> What if we just have an option on virtual host base to drop path
>> parameters of the following kind
>>
>> s#/([.]{0,2})(;[^/]*)/#/$1/g
>>
>> do the usual shrinking operation afterwards and just process them
>> afterwards as usual.
> 
> I think it makes sense to have it there but separated from the
> servletnormalizecheck because that changes the whole  mapping
> So I will add something like MergeSlashes which will map
> http://localhost:8000/docs/..;foo=bar/;foo=bar/test/index.jsp
> to /test
> And arrange the proxy so that /docs/..;foo=bar/;foo=bar/test/index.jsp
> is sent to the back-end.

That sounds good to me. That is the expected mapping from a servlet
perspective.

Thanks for all your efforts on this.

Mark


Re: hardening mod_write and mod_proxy like mod_jk with servletnormalize

2020-06-11 Thread Yann Ylavic
On Thu, Jun 11, 2020 at 9:50 AM Yann Ylavic  wrote:
>
> We need a way to forward non %-decoded URLs upto mod_proxy (reverse)
> if we want to normalize a second time..

IOW, this block in ap_process_request_internal():

/* Ignore URL unescaping for proxy requests */
if (!r->proxyreq && r->parsed_uri.path) {
d = ap_get_core_module_config(r->per_dir_config);
if (d->allow_encoded_slashes) {
access_status = ap_unescape_url_keep2f(r->parsed_uri.path,
   d->decode_encoded_slashes);
}
else {
access_status = ap_unescape_url(r->parsed_uri.path);
}
if (access_status) {
if (access_status == HTTP_NOT_FOUND) {
if (! d->allow_encoded_slashes) {
ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, APLOGNO(00026)
  "found %%2f (encoded '/') in URI "
  "(decoded='%s'), returning 404",
  r->uri);
}
}
return access_status;
}
}

Should go _after_ the following:

if ((access_status = ap_run_translate_name(r))) {
return decl_die(access_status, "translate", r);
}


But it looks like a breaking change for 2.4.x..


Re: hardening mod_write and mod_proxy like mod_jk with servletnormalize

2020-06-11 Thread Yann Ylavic
On Thu, Jun 11, 2020 at 8:52 AM jean-frederic clere  wrote:
>
> Should I commit my first proposal (it is easily backportable to 2.4.x)
> and later work on the next one?

How about something like the attached patch?

It's a new single ap_normalize_path() helper with options (like
AP_NORMALIZE_MERGE_SLASHES and AP_NORMALIZE_PATH_PARAMETERS), which we
can use in multiple places to replace ap_getparents(). The patch is
without the mod_rewrite bits (for now), and uses the "mapping=servlet"
syntax for the new proxypass parameter (which I found more
extensible).

The issue with re-calling ap_getparents() in ap_proxy_trans_match()
(or ap_normalize_path() in my patch still) is that it happens after
%-decoding, so bets are off.
For instance "/docs/..%3Bfoo=bar/%3Bfoo=bar/test/index.jsp" is not the
same as "http://localhost:8000/docs/..;foo=bar/;foo=bar/test/index.jsp";
and shouldn't be matched the same by mapping=servlet.

We need a way to forward non %-decoded URLs upto mod_proxy (reverse)
if we want to normalize a second time..


Regards;
Yann.
Index: include/httpd.h
===
--- include/httpd.h	(revision 1878328)
+++ include/httpd.h	(working copy)
@@ -1779,8 +1779,21 @@ AP_DECLARE(void) ap_no2slash(char *name)
 AP_DECLARE(void) ap_no2slash_ex(char *name, int is_fs_path)
  AP_FN_ATTR_NONNULL_ALL;
 
+#define AP_NORMALIZE_NOT_ABOVE_ROOT (1u <<  0)
+#define AP_NORMALIZE_MERGE_SLASHES  (1u <<  1)
+#define AP_NORMALIZE_PATH_PARAMETERS(1u <<  2)
 
 /**
+ * Remove all , /./ and /xx/../ substrings from a path, and more
+ * depending on passed in flags.
+ * @param path The path to normalize
+ * @param flags bitmask of AP_NORMALIZE_* flags
+ * @return non-zero on success
+ */
+AP_DECLARE(int) ap_normalize_path(char *path, unsigned int flags)
+AP_FN_ATTR_NONNULL((1));
+
+/**
  * Remove all ./ and xx/../ substrings from a file name. Also remove
  * any leading ../ or /../ substrings.
  * @param name the file name to parse
Index: modules/dav/main/util.c
===
--- modules/dav/main/util.c	(revision 1878328)
+++ modules/dav/main/util.c	(working copy)
@@ -664,7 +664,11 @@ static dav_error * dav_process_if_header(request_r
 /* note that parsed_uri.path is allocated; we can trash it */
 
 /* clean up the URI a bit */
-ap_getparents(parsed_uri.path);
+if (!ap_normalize_path(parsed_uri.path, 0)) {
+return dav_new_error(r->pool, HTTP_BAD_REQUEST,
+ DAV_ERR_IF_TAGGED, rv,
+ "Invalid URI path tagged If-header.");
+}
 
 /* the resources we will compare to have unencoded paths */
 if (ap_unescape_url(parsed_uri.path) != OK) {
Index: modules/generators/mod_autoindex.c
===
--- modules/generators/mod_autoindex.c	(revision 1878328)
+++ modules/generators/mod_autoindex.c	(working copy)
@@ -1266,8 +1266,7 @@ static struct ent *make_parent_entry(apr_int32_t a
 if (!(p->name = ap_make_full_path(r->pool, r->uri, "../"))) {
 return (NULL);
 }
-ap_getparents(p->name);
-if (!*p->name) {
+if (!ap_normalize_path(p->name, 0)) {
 return (NULL);
 }
 
Index: modules/proxy/mod_proxy.c
===
--- modules/proxy/mod_proxy.c	(revision 1878328)
+++ modules/proxy/mod_proxy.c	(working copy)
@@ -753,6 +753,25 @@ PROXY_DECLARE(int) ap_proxy_trans_match(request_re
 mismatch = 1;
 use_uri = r->uri;
 }
+else if (!nocanon && (ent->flags & PROXYPASS_MAPPING_SERVLET)) {
+char *uri = apr_pstrdup(r->pool, r->uri);
+
+/* check against the backend servlet url */
+if (!ap_normalize_path(uri, AP_NORMALIZE_MERGE_SLASHES |
+AP_NORMALIZE_PATH_PARAMETERS)) {
+ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r,
+  "servlet normalization failed for '%s'", r->uri);
+return HTTP_INTERNAL_SERVER_ERROR;
+}
+
+if (!alias_match(uri, ent->fake)) {
+ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, APLOGNO()
+  "servlet normalization for '%s' mismatch; "
+  "declining '%s'", r->uri, uri);
+return DECLINED;
+}
+}
+
 found = apr_pstrcat(r->pool, "proxy:", real, use_uri + len, NULL);
 }
 }
@@ -1806,6 +1825,9 @@ static const char *
 else if (!strcasecmp(word,"noquery")) {
 flags |= PROXYPASS_NOQUERY;
 }
+else if (!strcasecmp(word,"mapping=servlet")) {
+flags |= PRO