From fca9eabafe45095a2102ca101ac230b03ea82613 Mon Sep 17 00:00:00 2001 From: ivanr Date: Thu, 3 May 2007 12:09:24 +0000 Subject: [PATCH] Merged the PDF XSS protection functionality into ModSecurity. --- CHANGES | 2 + apache2/Makefile.win | 2 +- apache2/apache2.h | 2 +- apache2/apache2_config.c | 121 ++++++++++ apache2/mod_security2.c | 28 ++- apache2/modsecurity.h | 7 + apache2/modules.mk | 4 +- apache2/pdf_protect.c | 463 +++++++++++++++++++++++++++++++++++++++ apache2/pdf_protect.h | 21 ++ 9 files changed, 643 insertions(+), 7 deletions(-) create mode 100644 apache2/pdf_protect.c create mode 100644 apache2/pdf_protect.h diff --git a/CHANGES b/CHANGES index 69175ecf..1bc0c73a 100644 --- a/CHANGES +++ b/CHANGES @@ -2,6 +2,8 @@ ?? ??? 2007 - 2.2.0-trunk ------------------------- + * Merged the PDF XSS protection functionality into ModSecurity. + * Exported API for registering custom variables. Example in api directory. * Added experimental variables RESPONSE_CONTENT_LENGTH, RESPONSE_CONTENT_TYPE, diff --git a/apache2/Makefile.win b/apache2/Makefile.win index 59dd226c..8691fb6b 100644 --- a/apache2/Makefile.win +++ b/apache2/Makefile.win @@ -23,7 +23,7 @@ LIBS = $(BASE)\lib\libhttpd.lib $(BASE)\lib\libapr.lib $(BASE)\lib\libaprutil.li OBJS = mod_security2.obj apache2_config.obj apache2_io.obj apache2_util.obj \ re.obj re_operators.obj re_actions.obj re_tfns.obj re_variables.obj \ msc_logging.obj msc_xml.obj msc_multipart.obj modsecurity.obj msc_parsers.obj \ - msc_util.obj msc_pcre.obj persist_dbm.obj msc_reqbody.obj + msc_util.obj msc_pcre.obj persist_dbm.obj msc_reqbody.obj pdf_protec.obj all: $(DLL) diff --git a/apache2/apache2.h b/apache2/apache2.h index 117e1b79..6f8d68e6 100644 --- a/apache2/apache2.h +++ b/apache2/apache2.h @@ -2,7 +2,7 @@ * ModSecurity for Apache 2.x, http://www.modsecurity.org/ * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) * - * $Id: apache2.h,v 1.1.1.1 2006/10/14 09:30:43 ivanr Exp $ + * $Id$ * * You should have received a copy of the licence along with this * program (stored in the file "LICENSE"). If the file is missing, diff --git a/apache2/apache2_config.c b/apache2/apache2_config.c index 3d741553..c0c4d548 100644 --- a/apache2/apache2_config.c +++ b/apache2/apache2_config.c @@ -84,6 +84,13 @@ void *create_directory_config(apr_pool_t *mp, char *path) { /* Content injection. */ dcfg->content_injection_enabled = NOT_SET; + /* PDF XSS protection. */ + dcfg->pdfp_enabled = NOT_SET; + dcfg->pdfp_secret = NOT_SET_P; + dcfg->pdfp_timeout = NOT_SET; + dcfg->pdfp_token_name = NOT_SET_P; + dcfg->pdfp_only_get = NOT_SET; + return dcfg; } @@ -363,6 +370,18 @@ void *merge_directory_configs(apr_pool_t *mp, void *_parent, void *_child) { merged->content_injection_enabled = (child->content_injection_enabled == NOT_SET ? parent->content_injection_enabled : child->content_injection_enabled); + /* PDF XSS protection. */ + merged->pdfp_enabled = (child->pdfp_enabled == NOT_SET + ? parent->pdfp_enabled : child->pdfp_enabled); + merged->pdfp_secret = (child->pdfp_secret == NOT_SET_P + ? parent->pdfp_secret : child->pdfp_secret); + merged->pdfp_timeout = (child->pdfp_timeout == NOT_SET + ? parent->pdfp_timeout : child->pdfp_timeout); + merged->pdfp_token_name = (child->pdfp_token_name == NOT_SET_P + ? parent->pdfp_token_name : child->pdfp_token_name); + merged->pdfp_only_get = (child->pdfp_only_get == NOT_SET + ? parent->pdfp_only_get : child->pdfp_only_get); + return merged; } @@ -424,6 +443,13 @@ void init_directory_config(directory_config *dcfg) { /* Content injection. */ if (dcfg->content_injection_enabled == NOT_SET) dcfg->content_injection_enabled = 0; + + /* PDF XSS protection. */ + if (dcfg->pdfp_enabled == NOT_SET) dcfg->pdfp_enabled = 0; + if (dcfg->pdfp_secret == NOT_SET_P) dcfg->pdfp_secret = NULL; + if (dcfg->pdfp_timeout == NOT_SET) dcfg->pdfp_timeout = 10; + if (dcfg->pdfp_token_name == NOT_SET_P) dcfg->pdfp_token_name = "PDFPTOKEN"; + if (dcfg->pdfp_only_get == NOT_SET) dcfg->pdfp_only_get = 0; } /** @@ -1104,6 +1130,61 @@ static const char *cmd_web_app_id(cmd_parms *cmd, void *_dcfg, const char *p1) { return NULL; } +/* -- PDF Protection configuration -- */ + +static const char *cmd_pdf_protect(cmd_parms *cmd, void *_dcfg, int flag) { + directory_config *dcfg = (directory_config *)_dcfg; + if (dcfg == NULL) return NULL; + + dcfg->pdfp_enabled = flag; + + return NULL; +} + +static const char *cmd_pdf_protect_secret(cmd_parms *cmd, void *_dcfg, + const char *p1) +{ + directory_config *dcfg = (directory_config *)_dcfg; + if (dcfg == NULL) return NULL; + + dcfg->pdfp_secret = p1; + + return NULL; +} + +static const char *cmd_pdf_protect_timeout(cmd_parms *cmd, void *_dcfg, + const char *p1) +{ + directory_config *dcfg = (directory_config *)_dcfg; + if (dcfg == NULL) return NULL; + + dcfg->pdfp_timeout = atoi(p1); + + return NULL; +} + +static const char *cmd_pdf_protect_token_name(cmd_parms *cmd, void *_dcfg, + const char *p1) +{ + directory_config *dcfg = (directory_config *)_dcfg; + if (dcfg == NULL) return NULL; + + dcfg->pdfp_token_name = p1; + + return NULL; +} + +static const char *cmd_pdf_protect_intercept_get_only(cmd_parms *cmd, void *_dcfg, + int flag) +{ + directory_config *dcfg = (directory_config *)_dcfg; + if (dcfg == NULL) return NULL; + + dcfg->pdfp_only_get = flag; + + return NULL; +} + /* -- Configuration directives definitions -- */ @@ -1403,5 +1484,45 @@ const command_rec module_directives[] = { "" // TODO ), + AP_INIT_FLAG ( + "SecPdfProtect", + cmd_pdf_protect, + NULL, + RSRC_CONF, + "enable PDF protection module." + ), + + AP_INIT_TAKE1 ( + "SecPdfProtectSecret", + cmd_pdf_protect_secret, + NULL, + RSRC_CONF, + "secret that will be used to construct protection tokens." + ), + + AP_INIT_TAKE1 ( + "SecPdfProtectTimeout", + cmd_pdf_protect_timeout, + NULL, + RSRC_CONF, + "duration for which protection tokens will be valid." + ), + + AP_INIT_TAKE1 ( + "SecPdfProtectTokenName", + cmd_pdf_protect_token_name, + NULL, + RSRC_CONF, + "name of the protection token. The name 'PDFTOKEN' is used by default." + ), + + AP_INIT_FLAG ( + "SecPdfProtectInterceptGETOnly", + cmd_pdf_protect_intercept_get_only, + NULL, + RSRC_CONF, + "whether or not to intercept only GET requess." + ), + { NULL } }; diff --git a/apache2/mod_security2.c b/apache2/mod_security2.c index 03dcbdfe..db9d9918 100644 --- a/apache2/mod_security2.c +++ b/apache2/mod_security2.c @@ -17,6 +17,7 @@ #include "modsecurity.h" #include "apache2.h" +#include "pdf_protect.h" #include "msc_logging.h" #include "msc_util.h" @@ -585,6 +586,15 @@ static int hook_request_late(request_rec *r) { init_directory_config(msr->txcfg); + /* Perform the PDF tests now. */ + rc = pdfp_check(msr); + if (rc > 0) { + /* The PDF protection module has decided it needs to + * redirect the current transaction. So we let it do that. + */ + return rc; + } + if (msr->txcfg->is_enabled == 0) { if (msr->txcfg->debuglog_level >= 4) { msr_log(msr, 4, "Processing disabled, skipping (hook request_late)."); @@ -831,12 +841,18 @@ static int hook_log_transaction(request_rec *r) { static void hook_insert_filter(request_rec *r) { modsec_rec *msr = NULL; - /* Find the transaction context and make sure we are - * supposed to proceed. - */ + /* Find the transaction context first. */ msr = retrieve_tx_context(r); if (msr == NULL) return; + /* We always add the PDF XSS protection filter. */ + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Hook insert_filter: Adding PDF XSS protection output filter (r %x).", r); + } + + ap_add_output_filter("PDFP_OUT", msr, r, r->connection); + + /* Only proceed to add the second filter if the engine is enabled. */ if (msr->txcfg->is_enabled == 0) { if (msr->txcfg->debuglog_level >= 4) { msr_log(msr, 4, "Hook insert_filter: Processing disabled, skipping."); @@ -844,10 +860,12 @@ static void hook_insert_filter(request_rec *r) { return; } + /* Add the input filter, but only if we need it to run. */ if (msr->if_status == IF_STATUS_WANTS_TO_RUN) { if (msr->txcfg->debuglog_level >= 4) { msr_log(msr, 4, "Hook insert_filter: Adding input forwarding filter (r %x).", r); } + ap_add_input_filter("MODSECURITY_IN", msr, r, r->connection); } @@ -859,6 +877,7 @@ static void hook_insert_filter(request_rec *r) { if (msr->txcfg->debuglog_level >= 4) { msr_log(msr, 4, "Hook insert_filter: Adding output filter (r %x).", r); } + ap_add_output_filter("MODSECURITY_OUT", msr, r, r->connection); } } @@ -994,6 +1013,9 @@ static void register_hooks(apr_pool_t *mp) { NULL, AP_FTYPE_CONTENT_SET); ap_register_output_filter("MODSECURITY_OUT", output_filter, NULL, AP_FTYPE_CONTENT_SET); + + ap_register_output_filter("PDFP_OUT", pdfp_output_filter, + NULL, AP_FTYPE_CONTENT_SET); } /* Defined in apache2_config.c */ diff --git a/apache2/modsecurity.h b/apache2/modsecurity.h index 6bfadf04..37135172 100644 --- a/apache2/modsecurity.h +++ b/apache2/modsecurity.h @@ -395,6 +395,13 @@ struct directory_config { /* Content injection. */ int content_injection_enabled; + + /* PDF XSS Protection. */ + int pdfp_enabled; + const char *pdfp_secret; + int pdfp_timeout; + const char *pdfp_token_name; + int pdfp_only_get; }; struct error_message { diff --git a/apache2/modules.mk b/apache2/modules.mk index 60707cee..f8c97b94 100644 --- a/apache2/modules.mk +++ b/apache2/modules.mk @@ -2,10 +2,10 @@ MOD_SECURITY2 = mod_security2 apache2_config apache2_io apache2_util \ re re_operators re_actions re_tfns re_variables \ msc_logging msc_xml msc_multipart modsecurity msc_parsers msc_util msc_pcre \ - persist_dbm msc_reqbody + persist_dbm msc_reqbody pdf_protect H = re.h modsecurity.h msc_logging.h msc_multipart.h msc_parsers.h \ - msc_pcre.h msc_util.h msc_xml.h persist_dbm.h apache2.h + msc_pcre.h msc_util.h msc_xml.h persist_dbm.h apache2.h pdf_protect.h ${MOD_SECURITY2:=.slo}: ${H} ${MOD_SECURITY2:=.lo}: ${H} diff --git a/apache2/pdf_protect.c b/apache2/pdf_protect.c new file mode 100644 index 00000000..c9341efa --- /dev/null +++ b/apache2/pdf_protect.c @@ -0,0 +1,463 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id$ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#include "modsecurity.h" +// #include "apache2.h" +#include "pdf_protect.h" + +#include +#include "apr_sha1.h" + +#define DEFAULT_TIMEOUT 10 +#define DEFAULT_TOKEN_NAME "PDFPTOKEN" +#define ATTACHMENT_MIME_TYPE "application/octet-stream" +#define NOTE_TWEAK_HEADERS "pdfp-note-tweakheaders" +#define NOTE_DONE "pdfp-note-done" +#define REDIRECT_STATUS HTTP_TEMPORARY_REDIRECT +#define DISPOSITION_VALUE "attachment;" + +// TODO We need ID and REV values for the PDF XSS alert. + +// TODO It would be nice if the user could choose the ID/REV/SEVERITY/MESSAGE, etc. + +static char *encode_sha1_base64(apr_pool_t *mp, const char *data) { + unsigned char digest[APR_SHA1_DIGESTSIZE]; + apr_sha1_ctx_t context; + + /* Calculate the hash first. */ + apr_sha1_init(&context); + apr_sha1_update(&context, data, strlen(data)); + apr_sha1_final(digest, &context); + + /* Now transform with transport-friendly hex encoding. */ + return bytes2hex(mp, digest, APR_SHA1_DIGESTSIZE); +} + +static char *create_hash(modsec_rec *msr, + const char *time_string) +{ + const char *content = NULL; + + if (msr->txcfg->pdfp_secret == NULL) { + msr_log(msr, 1, "PdfProtect: Unable to generate hash. Please configure SecPdfProtectSecret."); + return NULL; + } + + /* Our protection token is made out of the client's IP + * address, the secret key, and the token expiry time. + */ + content = apr_pstrcat(msr->mp, msr->remote_addr, msr->txcfg->pdfp_secret, + time_string, NULL); + if (content == NULL) return NULL; + + return encode_sha1_base64(msr->mp, content); +} + +/** + * + */ +static char *create_token(modsec_rec *msr) { + unsigned int current_time; + const char *time_string = NULL; + const char *hash = NULL; + int timeout = DEFAULT_TIMEOUT; + + if (msr->txcfg->pdfp_timeout != -1) { + timeout = msr->txcfg->pdfp_timeout; + } + + current_time = apr_time_sec(apr_time_now()); + time_string = apr_psprintf(msr->mp, "%i", current_time + timeout); + if (time_string == NULL) return NULL; + + hash = create_hash(msr, time_string); + if (hash == NULL) return NULL; + + return apr_pstrcat(msr->mp, hash, "|", time_string, NULL); +} + +/** + * + */ +static char *construct_new_uri(modsec_rec *msr) { + const char *token_parameter = NULL; + const char *new_uri = NULL; + const char *token = NULL; + const char *token_name = DEFAULT_TOKEN_NAME; + + token = create_token(msr); + if (token == NULL) return NULL; + + if (msr->txcfg->pdfp_token_name != NULL) { + token_name = msr->txcfg->pdfp_token_name; + } + + token_parameter = apr_pstrcat(msr->mp, token_name, "=", token, NULL); + if (token_parameter == NULL) return NULL; + + if (msr->r->args == NULL) { /* No other parameters. */ + new_uri = apr_pstrcat(msr->mp, msr->r->uri, "?", token_parameter, "#PDFP", NULL); + } else { /* Preserve existing paramters. */ + new_uri = apr_pstrcat(msr->mp, msr->r->uri, "?", msr->r->args, "&", + token_parameter, "#PDFP", NULL); + } + + return (char *)new_uri; +} + +/** + * + */ +static char *extract_token(modsec_rec *msr) { + char *search_string = NULL; + char *p = NULL, *t = NULL; + const char *token_name = DEFAULT_TOKEN_NAME; + + if ((msr->r == NULL)||(msr->r->args == NULL)) { + return NULL; + } + + if (msr->txcfg->pdfp_token_name != NULL) { + token_name = msr->txcfg->pdfp_token_name; + } + + search_string = apr_pstrcat(msr->mp, msr->txcfg->pdfp_token_name, "=", NULL); + if (search_string == NULL) return NULL; + + p = strstr(msr->r->args, search_string); + if (p == NULL) return NULL; + + t = p = p + strlen(search_string); + while ((*t != '\0')&&(*t != '&')) t++; + + return apr_pstrmemdup(msr->mp, p, t - p); +} + +/** + * + */ +static int validate_time_string(const char *time_string) { + char *p = (char *)time_string; + + while(*p != '\0') { + if (!isdigit(*p)) return 0; + p++; + } + + return 1; +} + +/** + * + */ +static int verify_token(modsec_rec *msr, const char *token, char **error_msg) { + unsigned int current_time, expiry_time; + const char *time_string = NULL; + const char *given_hash = NULL; + const char *hash = NULL; + const char *p = NULL; + + if (error_msg == NULL) return 0; + *error_msg = NULL; + + /* Split token into its parts - hash and expiry time. */ + p = strstr(token, "|"); + if (p == NULL) return 0; + + given_hash = apr_pstrmemdup(msr->mp, token, p - token); + time_string = p + 1; + if (!validate_time_string(time_string)) { + *error_msg = apr_psprintf(msr->mp, "PdfProtect: Invalid time string: %s", + log_escape_nq(msr->mp, time_string)); + return 0; + } + expiry_time = atoi(time_string); + + /* Check the hash. */ + hash = create_hash(msr, time_string); + if (strcmp(given_hash, hash) != 0) { + *error_msg = apr_psprintf(msr->mp, "PdfProtect: Invalid hash: %s (expected %s)", + log_escape_nq(msr->mp, given_hash), log_escape_nq(msr->mp, hash)); + return 0; + } + + /* Check time. */ + current_time = apr_time_sec(apr_time_now()); + if (current_time > expiry_time) { + *error_msg = apr_psprintf(msr->mp, "PdfProtect: Token has expired."); + return 0; + } + + return 1; +} + +/** + * + */ +static apr_status_t send_error_bucket(ap_filter_t *f, int status) { + apr_bucket_brigade *brigade = NULL; + apr_bucket *bucket = NULL; + + brigade = apr_brigade_create(f->r->pool, f->r->connection->bucket_alloc); + if (brigade == NULL) return APR_EGENERAL; + bucket = ap_bucket_error_create(status, NULL, f->r->pool, f->r->connection->bucket_alloc); + if (bucket == NULL) return APR_EGENERAL; + APR_BRIGADE_INSERT_TAIL(brigade, bucket); + + f->r->connection->keepalive = AP_CONN_CLOSE; + return ap_pass_brigade(f->next, brigade); +} + +/** + * + */ +apr_status_t pdfp_output_filter(ap_filter_t *f, apr_bucket_brigade *bb_in) { + modsec_rec *msr = (modsec_rec *)f->ctx; + + if (msr == NULL) { + ap_log_error(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, 0, f->r->server, + "ModSecurity: Internal Error: msr is null in PDF output filter."); + ap_remove_output_filter(f); + return send_error_bucket(f, HTTP_INTERNAL_SERVER_ERROR); + } + + if (msr->txcfg->pdfp_enabled == 1) { + const char *h_content_type = apr_table_get(f->r->headers_out, "Content-Type"); + + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "PdfProtect: r->content_type=%s, header C-T=%s", + f->r->content_type, h_content_type); + } + + /* Have we been asked to tweak the headers? */ + if (apr_table_get(f->r->notes, NOTE_TWEAK_HEADERS) != NULL) { + /* Yes! */ + apr_table_set(f->r->headers_out, "Content-Disposition", DISPOSITION_VALUE); + f->r->content_type = ATTACHMENT_MIME_TYPE; + } + + /* Check if we've already done the necessary work in + * the first phase. + */ + if (apr_table_get(f->r->notes, NOTE_DONE) != NULL) { + /* We have, so return straight away. */ + ap_remove_output_filter(f); + return ap_pass_brigade(f->next, bb_in); + } + + /* Proceed to detect dynamically-generated PDF files. */ + if (((f->r->content_type != NULL)&&(strcasecmp(f->r->content_type, "application/pdf") == 0)) + || ((h_content_type != NULL)&&(strcasecmp(h_content_type, "application/pdf") == 0))) + { + request_rec *r = f->r; + const char *token = NULL; + + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "PdfProtect: Detected a dynamically-generated PDF in %s", + r->uri); + } + + /* Is this a non-GET request? */ + if ((f->r->method_number != M_GET)&& + ((msr->txcfg->pdfp_only_get == 1)||(msr->txcfg->pdfp_only_get == -1)) + ) { + /* This is a non-GET request and we have been configured + * not to intercept it. We are not going to do that but + * we are going to tweak the headers to force download. + */ + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "PdfProtect: Forcing download of a dynamically " + "generated PDF file and non-GET method."); + } + + apr_table_set(f->r->headers_out, "Content-Disposition", DISPOSITION_VALUE); + f->r->content_type = ATTACHMENT_MIME_TYPE; + + ap_remove_output_filter(f); + return ap_pass_brigade(f->next, bb_in); + } + + /* Locate the protection token. */ + token = extract_token(msr); + + if (token == NULL) { /* No token. */ + char *new_uri = NULL; + + /* Create a new URI with the protection token inside. */ + new_uri = construct_new_uri(msr); + if (new_uri != NULL) { + /* Redirect user to the new URI. */ + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "PdfProtect: PDF request without a token - " + "redirecting to %s.", new_uri); + } + + apr_table_set(r->headers_out, "Location", new_uri); + return send_error_bucket(f, REDIRECT_STATUS); + } + } else { /* Token found. */ + char *my_error_msg = NULL; + + /* Verify the token is valid. */ + + if (verify_token(msr, token, &my_error_msg)) { /* Valid. */ + /* Do nothing - serve the PDF file. */ + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "PdfProtect: PDF request with a valid token " + "- serving PDF file normally."); + } + + /* Fall through. */ + } else { /* Not valid. */ + /* The token is not valid. We will tweak the response + * to prevent the PDF file from opening in the browser. + */ + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 9, "PdfProtect: PDF request with an invalid token " + "- serving file as attachment."); + } + + apr_table_set(r->headers_out, "Content-Disposition", DISPOSITION_VALUE); + r->content_type = ATTACHMENT_MIME_TYPE; + + /* Fall through. */ + } + } + } + } + + ap_remove_output_filter(f); + + return ap_pass_brigade(f->next, bb_in); +} + +/** + * + */ +int DSOLOCAL pdfp_check(modsec_rec *msr) { + const char *token = NULL; + directory_config *cfg = NULL; + char *uri = NULL; + char *p = NULL; + + if (msr->txcfg->pdfp_enabled != 1) { + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "PdfProtect: Not enabled here."); + } + return 0; + } + + /* Then determine whether we need to act at + * all. If the request is not for a PDF file + * return straight away. + */ + + if (msr->r->uri == NULL) { + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "PdfProtect: Unable to inspect URI because it is NULL."); + } + /* TODO Should we return -1 instead? */ + return 0; + } + + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "PdfProtect: URI=%s, filename=%s, QUERY_STRING=%s.", + msr->r->uri, msr->r->filename, msr->r->args); + } + + uri = apr_pstrdup(msr->mp, msr->r->uri); + if (uri == NULL) return -1; + ap_str_tolower(uri); + + /* Attempt to figure out if this is a request for a PDF file. We are + * going to be liberal and look for the extension anywhere in the URI, + * not just at the end. + */ + p = strstr(uri, ".pdf"); + if (p == NULL) { + /* We do not think this is a PDF file. */ + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "PdfProtect: No indication in the URI this is a " + "request for a PDF file."); + } + return 0; + } + + /* Ignore request methods other than GET if + * configured to do so. + */ + if ((msr->r->method_number != M_GET)&&(cfg->pdfp_only_get != 0)) { + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "PdfProtect: Configured not to intercept non-GET requests " + "(method=%s/%i).", msr->r->method, msr->r->method_number); + } + return 0; + } + + /* We make a note for ourselves that we've already handled + * the request. + */ + apr_table_set(msr->r->notes, NOTE_DONE, "1"); + + /* Locate the protection token. */ + token = extract_token(msr); + + if (token == NULL) { /* No token. */ + char *new_uri = NULL; + + /* Create a new URI with the protection token inside. */ + new_uri = construct_new_uri(msr); + if (new_uri == NULL) return DECLINED; + + /* Redirect user to the new URI. */ + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "PdfProtect: PDF request without a token - redirecting to %s.", + new_uri); + } + + apr_table_set(msr->r->headers_out, "Location", new_uri); + + return REDIRECT_STATUS; + } else { /* Token found. */ + char *my_error_msg = NULL; + + /* Verify the token is valid. */ + if (verify_token(msr, token, &my_error_msg)) { /* Valid. */ + /* Do nothing - serve the PDF file. */ + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "PdfProtect: PDF request with a valid token - " + "serving PDF file normally."); + } + return 0; + } else { /* Not valid. */ + /* The token is not valid. We will tweak the response + * to prevent the PDF file from opening in the browser. + */ + + // TODO Log alert + + /* Changing response headers before response generation phase takes + * place is not really reliable. Although we do this we also make + * a note for ourselves (in the output filter) to check the headers + * again just before they are sent back to the end user. + */ + apr_table_set(msr->r->headers_out, "Content-Disposition", DISPOSITION_VALUE); + msr->r->content_type = ATTACHMENT_MIME_TYPE; + apr_table_set(msr->r->notes, NOTE_TWEAK_HEADERS, "1"); + + /* Proceed with response (PDF) generation. */ + return 0; + } + } + + return 0; +} diff --git a/apache2/pdf_protect.h b/apache2/pdf_protect.h new file mode 100644 index 00000000..3502a6e1 --- /dev/null +++ b/apache2/pdf_protect.h @@ -0,0 +1,21 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2007 Thinking Stone (http://www.thinkingstone.com) + * + * $Id$ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ + +#ifndef _PDF_PROTECT_H_ +#define _PDF_PROTECT_H_ + +apr_status_t DSOLOCAL pdfp_output_filter(ap_filter_t *f, apr_bucket_brigade *bb_in); + +int DSOLOCAL pdfp_check(modsec_rec *msr); + +#endif