File 0014-Add-SymlinkProtect-and-SymlinkProtectRoot-functional.patch of Package ea-apache2
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Cory McIntire <cory@cpanel.net>
Date: Thu, 12 Jul 2018 15:50:51 -0500
Subject: [PATCH 14/21] Add SymlinkProtect and SymlinkProtectRoot functionality
---
include/http_core.h | 10 +++-
modules/mappers/mod_userdir.c | 31 +++++++---
modules/mappers/mod_userdir.dep | 1 +
server/core.c | 103 ++++++++++++++++++++++++++++++++
4 files changed, 135 insertions(+), 10 deletions(-)
diff --git a/include/http_core.h b/include/http_core.h
index 948034f..8673506 100644
--- a/include/http_core.h
+++ b/include/http_core.h
@@ -509,7 +509,7 @@ typedef unsigned long etag_components_t;
#define AP_CORE_MERGE_FLAG(field, to, base, over) to->field = \
over->field != AP_CORE_CONFIG_UNSET \
? over->field \
- : base->field
+ : base->field
/**
* @brief Server Signature Enumeration
@@ -672,7 +672,7 @@ typedef struct {
#define AP_CGI_PASS_AUTH_UNSET (2)
/** CGIPassAuth: Whether HTTP authorization headers will be passed to
* scripts as CGI variables; affects all modules calling
- * ap_add_common_vars(), as well as any others using this field as
+ * ap_add_common_vars(), as well as any others using this field as
* advice
*/
unsigned int cgi_pass_auth : 2;
@@ -752,6 +752,12 @@ typedef struct {
#define AP_HTTP_METHODS_REGISTERED 2
char http_methods;
unsigned int merge_slashes;
+ /* symlink protection */
+#define AP_SYMLINK_PROTECT_UNSET 0
+#define AP_SYMLINK_PROTECT_ENABLE 1
+#define AP_SYMLINK_PROTECT_DISABLE 2
+ int symlink_protect;
+ const char *symlink_protect_root;
apr_size_t flush_max_threshold;
apr_int32_t flush_max_pipelined;
diff --git a/modules/mappers/mod_userdir.c b/modules/mappers/mod_userdir.c
index 1ec0e90..6a0f44d 100644
--- a/modules/mappers/mod_userdir.c
+++ b/modules/mappers/mod_userdir.c
@@ -51,6 +51,7 @@
#include "apr_strings.h"
#include "apr_user.h"
+#include "apr_env.h"
#define APR_WANT_STRFUNC
#include "apr_want.h"
@@ -62,7 +63,9 @@
#include "ap_config.h"
#include "httpd.h"
#include "http_config.h"
+#include "http_core.h"
#include "http_request.h"
+#include "http_log.h"
#if !defined(WIN32) && !defined(OS2) && !defined(NETWARE)
#define HAVE_UNIX_SUEXEC
@@ -203,6 +206,7 @@ static int translate_userdir(request_rec *r)
const char *user, *dname;
char *redirect;
apr_finfo_t statbuf;
+ core_server_config *sconf;
/*
* If the URI doesn't match our basic pattern, we've nothing to do with
@@ -259,6 +263,8 @@ static int translate_userdir(request_rec *r)
* Special cases all checked, onward to normal substitution processing.
*/
+ sconf = ap_get_core_module_config(server_conf);
+
while (*userdirs) {
const char *userdir = ap_getword_conf(r->pool, &userdirs);
char *filename = NULL, *prefix = NULL;
@@ -313,18 +319,20 @@ static int translate_userdir(request_rec *r)
}
/*
- * Now see if it exists, or we're at the last entry. If we are at the
- * last entry, then use the filename generated (if there is one)
- * anyway, in the hope that some handler might handle it. This can be
- * used, for example, to run a CGI script for the user.
- */
+ * Now see if it exists, or we're at the last entry. If we are at the
+ * last entry, then use the filename generated (if there is one)
+ * anyway, in the hope that some handler might handle it. This can be
+ * used, for example, to run a CGI script for the user.
+ */
if (filename && (!*userdirs
- || ((rv = apr_stat(&statbuf, filename, APR_FINFO_MIN,
+ || ((rv = apr_stat(&statbuf, filename, (
+ sconf->symlink_protect == AP_SYMLINK_PROTECT_ENABLE) ?
+ APR_FINFO_NORM :
+ APR_FINFO_MIN,
r->pool)) == APR_SUCCESS
|| rv == APR_INCOMPLETE))) {
r->filename = apr_pstrcat(r->pool, filename, dname, NULL);
- ap_set_context_info(r, apr_pstrmemdup(r->pool, r->uri,
- dname - r->uri),
+ ap_set_context_info(r, apr_pstrmemdup(r->pool, r->uri, dname - r->uri),
filename);
/* XXX: Does this walk us around FollowSymLink rules?
* When statbuf contains info on r->filename we can save a syscall
@@ -333,6 +341,13 @@ static int translate_userdir(request_rec *r)
if (*userdirs && dname[0] == 0)
r->finfo = statbuf;
+ /* This is used later on to make sure the symlink exploit is not
+ * exploitable.
+ */
+ if (sconf->symlink_protect == AP_SYMLINK_PROTECT_ENABLE) {
+ apr_table_set(r->subprocess_env, "SPT_DOCROOT", filename);
+ }
+
/* For use in the get_suexec_identity phase */
apr_table_setn(r->notes, "mod_userdir_user", user);
diff --git a/modules/mappers/mod_userdir.dep b/modules/mappers/mod_userdir.dep
index 2aff834..fa229b1 100644
--- a/modules/mappers/mod_userdir.dep
+++ b/modules/mappers/mod_userdir.dep
@@ -12,6 +12,7 @@
"..\..\include\ap_regex.h"\
"..\..\include\ap_release.h"\
"..\..\include\apache_noprobes.h"\
+ "..\..\include\http_core.h"\
"..\..\include\http_config.h"\
"..\..\include\http_request.h"\
"..\..\include\httpd.h"\
diff --git a/server/core.c b/server/core.c
index 81f145f..4ee8905 100644
--- a/server/core.c
+++ b/server/core.c
@@ -21,6 +21,8 @@
#include "apr_hash.h"
#include "apr_thread_proc.h" /* for RLIMIT stuff */
#include "apr_random.h"
+#include "apr_env.h"
+#include "apr_file_io.h"
#include "apr_version.h"
#if APR_MAJOR_VERSION < 2
@@ -479,6 +481,7 @@ static void *create_core_server_config(apr_pool_t *a, server_rec *s)
if (!is_virtual) {
conf->ap_document_root = DOCUMENT_LOCATION;
+ conf->symlink_protect_root = "/var/www/html";
conf->access_name = DEFAULT_ACCESS_FNAME;
/* A mapping only makes sense in the global context */
@@ -543,6 +546,9 @@ static void *merge_core_server_configs(apr_pool_t *p, void *basev, void *virtv)
if (virt->ap_document_root)
conf->ap_document_root = virt->ap_document_root;
+ if (virt->symlink_protect_root)
+ conf->symlink_protect_root = virt->symlink_protect_root;
+
if (virt->access_name)
conf->access_name = virt->access_name;
@@ -589,6 +595,10 @@ static void *merge_core_server_configs(apr_pool_t *p, void *basev, void *virtv)
? virt->merge_trailers
: base->merge_trailers;
+ conf->symlink_protect = (virt->symlink_protect != AP_SYMLINK_PROTECT_UNSET)
+ ? virt->symlink_protect
+ : base->symlink_protect;
+
conf->protocols = ((virt->protocols->nelts > 0)?
virt->protocols : base->protocols);
conf->protocols_honor_order = ((virt->protocols_honor_order < 0)?
@@ -4430,6 +4440,30 @@ static const char *set_merge_trailers(cmd_parms *cmd, void *dummy, int arg)
return NULL;
}
+static const char *set_symlink_protect(cmd_parms *cmd, void *dummy, int arg)
+{
+ core_server_config *conf = ap_get_module_config(cmd->server->module_config,
+ &core_module);
+ conf->symlink_protect = (arg ? AP_SYMLINK_PROTECT_ENABLE :
+ AP_SYMLINK_PROTECT_DISABLE);
+ return NULL;
+}
+
+static const char *set_symlink_protect_root(cmd_parms *cmd, void *dummy,
+ const char *arg)
+{
+ char* value;
+ core_server_config *conf = ap_get_module_config(cmd->server->module_config,
+ &core_module);
+ if (arg == NULL) {
+ return "SymlinkProtectRoot must have a value";
+ }
+ value = apr_pstrdup(cmd->pool, arg);
+ conf->symlink_protect_root = value;
+
+ return NULL;
+}
+
/* Note --- ErrorDocument will now work from .htaccess files.
* The AllowOverride of Fileinfo allows webmasters to turn it off
*/
@@ -4707,6 +4741,10 @@ AP_INIT_TAKE1("ThreadStackSize", ap_mpm_set_thread_stacksize, NULL, RSRC_CONF,
AP_INIT_TAKE1("EnableExceptionHook", ap_mpm_set_exception_hook, NULL, RSRC_CONF,
"Controls whether exception hook may be called after a crash"),
#endif
+AP_INIT_FLAG("SymlinkProtect", set_symlink_protect, NULL, RSRC_CONF,
+ "Controls whether symlink protection will be active or not"),
+AP_INIT_TAKE1("SymlinkProtectRoot", set_symlink_protect_root, NULL, RSRC_CONF,
+ "Root directory of the symlink protect algorithm"),
AP_INIT_TAKE1("TraceEnable", set_trace_enable, NULL, RSRC_CONF,
"'on' (default), 'off' or 'extended' to trace request body content"),
AP_INIT_FLAG("MergeTrailers", set_merge_trailers, NULL, RSRC_CONF,
@@ -4872,6 +4910,9 @@ static int default_handler(request_rec *r)
int errstatus;
apr_file_t *fd = NULL;
apr_status_t status;
+ core_server_config *csconf;
+ apr_finfo_t post_open_dirstat;
+ apr_finfo_t post_open_finfo;
/* XXX if/when somebody writes a content-md5 filter we either need to
* remove this support or coordinate when to use the filter vs.
* when to use this code
@@ -4882,6 +4923,13 @@ static int default_handler(request_rec *r)
int bld_content_md5;
d = (core_dir_config *)ap_get_core_module_config(r->per_dir_config);
+
+ /* must fetch global configuration. First to determine if we are
+ * going to apply symlink protection, and second to check the document
+ * root user against the open file user if using symlink protection.
+ */
+ csconf = ap_get_module_config(r->server->module_config, &core_module);
+
bld_content_md5 = (d->content_md5 == AP_CONTENT_MD5_ON)
&& r->output_filters->frec->ftype != AP_FTYPE_RESOURCE;
@@ -4955,6 +5003,61 @@ static int default_handler(request_rec *r)
return HTTP_FORBIDDEN;
}
+ if (csconf->symlink_protect == AP_SYMLINK_PROTECT_ENABLE) {
+ /* This is where the magic is. If a user is trying to hit the apache
+ * symlink race condition, then we will know about it here.
+ */
+
+ const char *sp_docroot = apr_table_get(r->subprocess_env, "SPT_DOCROOT");
+ apr_status_t post_dirstat_rv;
+ apr_status_t post_fdstat_rv;
+
+ if (strcmp(csconf->ap_document_root, csconf->symlink_protect_root) == 0
+ && sp_docroot != NULL) {
+ /* This request is from mod_userdir. We need to stat what was stored in SPT_DOCROOT. */
+ post_dirstat_rv = apr_stat(&post_open_dirstat, sp_docroot,
+ APR_FINFO_USER | APR_FINFO_LINK, r->pool);
+ }
+ else {
+ /* This request matched a vhost. We need to stat ap_document_root. */
+ post_dirstat_rv = apr_stat(&post_open_dirstat, csconf->ap_document_root,
+ APR_FINFO_USER | APR_FINFO_LINK, r->pool);
+ }
+
+ post_fdstat_rv = apr_stat_fd(&post_open_finfo, fd, APR_FINFO_USER, r->pool);
+
+ if (((post_dirstat_rv != APR_SUCCESS && post_dirstat_rv != APR_INCOMPLETE)
+ || !(post_open_dirstat.valid & APR_FINFO_USER))
+ || ((post_fdstat_rv != APR_SUCCESS && post_fdstat_rv != APR_INCOMPLETE)
+ || !(post_open_finfo.valid & APR_FINFO_USER))) {
+ /* Then we couldn't stat either the directory root of the vhost
+ * (very unlikely) or we couldn't stat the open file descriptor
+ * (probably impossible).
+ */
+ ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r,
+ "Could not stat directory root or open file. Aborting request.");
+ apr_file_close(fd);
+ return HTTP_NOT_FOUND;
+ }
+
+
+ if (apr_uid_compare(r->finfo.user, post_open_dirstat.user)
+ != APR_SUCCESS || apr_uid_compare(post_open_finfo.user, r->finfo.user)
+ != APR_SUCCESS) {
+ /* Then we've caught a race condition abuser. */
+ ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r,
+ "Caught race condition abuser. attacker: %i, victim: %i"
+ " open file owner: %i, open file: %s", post_open_dirstat.user, r->finfo.user,
+ post_open_finfo.user, r->filename);
+
+ apr_file_close(fd);
+ /* Return 404 because we don't want an attacker to be able to test
+ * what files are where based on the return of an error.
+ */
+ return HTTP_NOT_FOUND;
+ }
+ }
+
ap_update_mtime(r, r->finfo.mtime);
ap_set_last_modified(r);
ap_set_etag_fd(r, fd);