-
Notifications
You must be signed in to change notification settings - Fork 97
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Advanced session hijacking mitigation #75
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -53,6 +53,8 @@ | |
#include "apr_strings.h" | ||
#include "apr_xml.h" | ||
|
||
#include "mod_ssl.h" | ||
|
||
#include "cas_saml_attr.h" | ||
|
||
/* Apache is NOT a well-behaved citizen. It unconditionally | ||
|
@@ -76,6 +78,9 @@ static apr_thread_mutex_t **ssl_locks; | |
static int ssl_num_locks; | ||
#endif /* defined(OPENSSL_THREADS) && APR_HAS_THREADS */ | ||
|
||
static APR_OPTIONAL_FN_TYPE(ssl_var_lookup) *mod_ssl_var_lookup; | ||
static APR_OPTIONAL_FN_TYPE(ssl_is_https) *mod_ssl_is_https; | ||
|
||
int cas_flock(apr_file_t *fileHandle, int lockOperation, request_rec *r) | ||
{ | ||
apr_os_file_t osFileHandle; | ||
|
@@ -116,6 +121,7 @@ void *cas_create_server_config(apr_pool_t *pool, server_rec *svr) | |
c->CASAttributeDelimiter = CAS_DEFAULT_ATTRIBUTE_DELIMITER; | ||
c->CASAttributePrefix = CAS_DEFAULT_ATTRIBUTE_PREFIX; | ||
c->CASAuthoritative = CAS_DEFAULT_AUTHORITATIVE; | ||
c->CASSecureSession = CAS_DEFAULT_SECURE_SESSION; | ||
|
||
cas_setURL(pool, &(c->CASLoginURL), CAS_DEFAULT_LOGIN_URL); | ||
cas_setURL(pool, &(c->CASValidateURL), CAS_DEFAULT_VALIDATE_URL); | ||
|
@@ -414,14 +420,16 @@ apr_byte_t cas_setURL(apr_pool_t *pool, apr_uri_t *uri, const char *url) | |
|
||
apr_byte_t isSSL(const request_rec *r) | ||
{ | ||
|
||
if (mod_ssl_is_https) { | ||
return mod_ssl_is_https(r->connection) != 0; | ||
} else { | ||
#ifdef APACHE2_0 | ||
if(apr_strnatcasecmp("https", ap_http_method(r)) == 0) | ||
#else | ||
if(apr_strnatcasecmp("https", ap_http_scheme(r)) == 0) | ||
#endif | ||
return TRUE; | ||
|
||
} | ||
return FALSE; | ||
} | ||
|
||
|
@@ -977,6 +985,7 @@ apr_byte_t readCASCacheFile(request_rec *r, cas_cfg *c, char *name, cas_cache_en | |
cache->secure = FALSE; | ||
cache->ticket = NULL; | ||
cache->attrs = NULL; | ||
cache->session = NULL; | ||
|
||
do { | ||
if(e == NULL) | ||
|
@@ -1015,6 +1024,8 @@ apr_byte_t readCASCacheFile(request_rec *r, cas_cfg *c, char *name, cas_cache_en | |
cache->secure = TRUE; | ||
else if (apr_strnatcasecmp(e->name, "ticket") == 0) | ||
cache->ticket = apr_pstrndup(r->pool, val, strlen(val)); | ||
else if (apr_strnatcasecmp(e->name, "session") == 0) | ||
cache->session = apr_pstrndup(r->pool, val, strlen(val)); | ||
else if (apr_strnatcasecmp(e->name, "attributes") == 0) { | ||
cas_attr_builder *builder = cas_attr_builder_new(r->pool, &(cache->attrs)); | ||
apr_xml_elem *attrs; | ||
|
@@ -1217,6 +1228,7 @@ apr_byte_t writeCASCacheEntry(request_rec *r, char *name, cas_cache_entry *cache | |
apr_file_printf(f, "<lastactive>%" APR_TIME_T_FMT "</lastactive>\n", cache->lastactive); | ||
apr_file_printf(f, "<path>%s</path>\n", apr_xml_quote_string(r->pool, cache->path, TRUE)); | ||
apr_file_printf(f, "<ticket>%s</ticket>\n", apr_xml_quote_string(r->pool, cache->ticket, TRUE)); | ||
apr_file_printf(f, "<session>%s</session>\n", apr_xml_quote_string(r->pool, cache->session, TRUE)); | ||
if(cache->attrs != NULL) { | ||
cas_saml_attr *a = cache->attrs; | ||
apr_file_printf(f, "<attributes>\n"); | ||
|
@@ -1251,6 +1263,87 @@ apr_byte_t writeCASCacheEntry(request_rec *r, char *name, cas_cache_entry *cache | |
return TRUE; | ||
} | ||
|
||
/* | ||
* hook function to retrieve the mod_ssl optional functions for looking up ssl state data | ||
* ssl_var_lookup allows us to fetch the ssl environment variables (used for accessing the SSL_SESSION_ID) | ||
* ssl_is_https is the mod_ssl to achieve the equivalent of isSSL | ||
*/ | ||
static void retrieve_mod_ssl_functions(void) { | ||
|
||
mod_ssl_var_lookup = APR_RETRIEVE_OPTIONAL_FN(ssl_var_lookup); | ||
mod_ssl_is_https = APR_RETRIEVE_OPTIONAL_FN(ssl_is_https); | ||
|
||
} | ||
|
||
/* | ||
* cas_secure_session() | ||
* | ||
* Build an internal session identifier for mod_auth_cas to use in session hijacking mitigation. | ||
* existing mitigations neglect full cookie theft and instead rely on cookie modifications. | ||
* The preferred method of session id generation is to borrow the SSL Session from mod_ssl | ||
* given that the SSL Session is not exposed to browser javascript engines we can assert | ||
* with a high degree of confidence that SSL_SESSION_ID will not likely be stolen. | ||
* In the event we negotiating via HTTP we attempt to create as secure an id as we can. | ||
* An HTTP id is comprised of the user's IP address which is acquired either via the useragent_ip | ||
* field of the apache request (if available in our current version) or from the apr_get_remote_host | ||
* function (which should be available in apache 2.x). This value is concatenated with the | ||
* user-agent header to create a semi unique internal identifier which should not change | ||
* during the session. | ||
* | ||
* Known issues: | ||
* In the event the apache server is running behind NAT or a load-balancer the ip address | ||
* obtained by ap_get_remote_host may be the same for all users. it is for this reason we | ||
* would rather obtain useragent_ip when available. | ||
* | ||
* In the event the above takes place theft of the user-agent can be used to spoof our users | ||
* ''' navigator.userAgent + document.cookie ''' | ||
* | ||
* In order to address these we would need other internal identifier similar to the SSL | ||
* session id which is unique and not exposed to the end-user | ||
* | ||
*/ | ||
char *cas_secure_session(request_rec *r) { | ||
|
||
static const char kNULL_ID[] = "[NULL]"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason to use [NULL] instead of treating the empty string as a null session ID and "(void *) 0" in its place? |
||
|
||
char *secure_id = NULL; | ||
|
||
// first pass (our preferred method) | ||
if (mod_ssl_var_lookup) { | ||
secure_id = mod_ssl_var_lookup(r->pool, r->server, r->connection, r, "SSL_SESSION_ID"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, I'm guessing this assumes that subsequent TLS negotiations will use TLS session resumption, which probably holds for most clients -- have you tested with mobile clients or firewalls / proxies / load balancers fronting a mod_auth_cas install that may behave differently? Do you offhand have any reference information on how long a client will cache a TLS session, and how that compares with the default values for the CAS session timeouts? |
||
} | ||
|
||
if (!secure_id || !strlen(secure_id)) { | ||
|
||
char *ip_addr = NULL; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How does this play with someone coming from a large NAT pool? |
||
char *user_agent = NULL; | ||
char *plaintext = NULL; | ||
|
||
// in apache 2.4 we are able to use the useragent_ip which should be the conneting host | ||
// this is a much better solution as it will not be covered by natted requests. | ||
#ifdef APACHE2_4 | ||
ip_addr = (char *)r->useragent_ip; | ||
#endif | ||
|
||
// if we are less than apache 2.4 or the useragent_ip field is NULL we can fallback to | ||
// fetching the ip from the ap_get_remote_host function | ||
if (!ip_addr) | ||
ip_addr = (char *)ap_get_remote_host(r->connection, r->connection->base_server->lookup_defaults, REMOTE_NOLOOKUP, NULL); | ||
|
||
user_agent = (char *)apr_table_get(r->headers_in, "User-Agent"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if this header is not present or empty? Does apr_pstrcat safely handle a NULL concatenation? (can't recall) |
||
|
||
plaintext = apr_pstrcat(r->pool, ip_addr, user_agent, NULL); | ||
|
||
secure_id = ap_md5(r->pool, (unsigned char *)plaintext); | ||
} | ||
|
||
// If everything else has failed up to this point we are going to unfortunately use "[NULL]" | ||
if (!secure_id) | ||
secure_id = (char *)apr_pstrdup(r->pool, kNULL_ID); | ||
|
||
return (char *)secure_id; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no need for cast here? |
||
} | ||
|
||
char *createCASCookie(request_rec *r, char *user, cas_saml_attr *attrs, char *ticket) | ||
{ | ||
char *path, *buf, *rv; | ||
|
@@ -1275,6 +1368,7 @@ char *createCASCookie(request_rec *r, char *user, cas_saml_attr *attrs, char *ti | |
e.secure = (isSSL(r) == TRUE ? 1 : 0); | ||
e.ticket = ticket; | ||
e.attrs = attrs; | ||
e.session = cas_secure_session(r); | ||
|
||
/* this may block since this reads from /dev/random - however, it hasn't been a problem in testing */ | ||
apr_generate_random_bytes((unsigned char *) buf, c->CASCookieEntropy); | ||
|
@@ -1629,6 +1723,23 @@ apr_byte_t isValidCASCookie(request_rec *r, cas_cfg *c, char *cookie, char **use | |
return FALSE; | ||
} | ||
|
||
/* | ||
* session hijacking mitigation through the cas entry "session" variable. | ||
* when possible we check for SSL_SESSION_ID matching (most secure) otherwise a number of other | ||
* session idenifiers are used. cas_secure_session() | ||
*/ | ||
if (c->CASSecureSession) { | ||
|
||
if (apr_strnatcmp(cas_secure_session(r), cache.session) != 0) { | ||
|
||
deleteCASCacheFile(r, cookie); | ||
if(c->CASDebug) | ||
ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Cookie '%s' originated from invalid source.", cookie); | ||
CASCleanCache(r, c); | ||
return FALSE; | ||
} | ||
} | ||
|
||
/* | ||
* mitigate session hijacking by not allowing cookies transmitted in the clear to be submitted | ||
* for HTTPS URLs and by voiding HTTPS cookies sent in the clear | ||
|
@@ -2301,7 +2412,7 @@ int cas_authorize(request_rec *r) | |
&auth_cas_module); | ||
|
||
ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, | ||
"Entering cas_authorize."); | ||
"Entering cas_authorize()"); | ||
|
||
if (!reqs_arr) { | ||
ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, | ||
|
@@ -2651,8 +2762,10 @@ void cas_register_hooks(apr_pool_t *p) | |
#else | ||
ap_hook_check_user_id(cas_authenticate, NULL, NULL, APR_HOOK_MIDDLE); | ||
#endif | ||
ap_hook_auth_checker(cas_authorize, NULL, authzSucc, APR_HOOK_MIDDLE); | ||
ap_hook_auth_checker(cas_authorize, NULL, authzSucc, APR_HOOK_MIDDLE); | ||
ap_register_input_filter("CAS", cas_in_filter, NULL, AP_FTYPE_RESOURCE); | ||
// mod_ssl_hook retrieve | ||
ap_hook_optional_fn_retrieve(retrieve_mod_ssl_functions, NULL, NULL, APR_HOOK_MIDDLE); | ||
} | ||
|
||
const command_rec cas_cmds [] = { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -63,6 +63,16 @@ | |
#endif | ||
#endif | ||
|
||
#ifndef APACHE2_4 | ||
#ifdef AP_SERVER_MAJORVERSION_NUMBER | ||
#ifdef AP_SERVER_MINORVERSION_NUMBER | ||
#if ((AP_SERVER_MAJORVERSION_NUMBER == 2) && (AP_SERVER_MINORVERSION_NUMBER >= 4)) | ||
#define APACHE2_4 | ||
#endif | ||
#endif | ||
#endif | ||
#endif | ||
|
||
#define CAS_DEFAULT_VERSION 2 | ||
#define CAS_DEFAULT_DEBUG FALSE | ||
#define CAS_DEFAULT_SCOPE NULL | ||
|
@@ -93,6 +103,7 @@ | |
#define CAS_DEFAULT_SCRUB_REQUEST_HEADERS NULL | ||
#define CAS_DEFAULT_SSO_ENABLED FALSE | ||
#define CAS_DEFAULT_AUTHORITATIVE FALSE | ||
#define CAS_DEFAULT_SECURE_SESSION TRUE | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Line 1713 mentions a TODO for enforcing, but here IIUC the default is set to enforcing mode? |
||
|
||
#define CAS_MAX_RESPONSE_SIZE 65536 | ||
#define CAS_MAX_ERROR_SIZE 1024 | ||
|
@@ -120,6 +131,7 @@ typedef struct cas_cfg { | |
unsigned int CASSSOEnabled; | ||
unsigned int CASAuthoritative; | ||
unsigned int CASValidateSAML; | ||
unsigned int CASSecureSession; | ||
char *CASCertificatePath; | ||
char *CASCookiePath; | ||
char *CASCookieDomain; | ||
|
@@ -151,6 +163,7 @@ typedef struct cas_cache_entry { | |
apr_byte_t secure; | ||
char *ticket; | ||
cas_saml_attr *attrs; | ||
char *session; | ||
} cas_cache_entry; | ||
|
||
typedef struct cas_curl_buffer { | ||
|
@@ -222,6 +235,8 @@ int check_merged_vhost_configs(apr_pool_t *pool, server_rec *s); | |
int check_vhost_config(apr_pool_t *pool, server_rec *s); | ||
int merged_vhost_configs_exist(server_rec *s); | ||
|
||
char *cas_secure_session(request_rec *r); | ||
|
||
#if (defined(OPENSSL_THREADS) && APR_HAS_THREADS) | ||
void cas_ssl_locking_callback(int mode, int type, const char *file, int line); | ||
#endif | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this parameter documented anywhere (most of the others at least have a mention in the README at least)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, looks like this isn't handled in any config parsing logic?