Imported Upstream version 2.5.11
[libapache-mod-security.git] / apache2 / pdf_protect.c
1 /*
2  * ModSecurity for Apache 2.x, http://www.modsecurity.org/
3  * Copyright (c) 2004-2009 Breach Security, Inc. (http://www.breach.com/)
4  *
5  * This product is released under the terms of the General Public Licence,
6  * version 2 (GPLv2). Please refer to the file LICENSE (included with this
7  * distribution) which contains the complete text of the licence.
8  *
9  * There are special exceptions to the terms and conditions of the GPL
10  * as it is applied to this software. View the full text of the exception in
11  * file MODSECURITY_LICENSING_EXCEPTION in the directory of this software
12  * distribution.
13  *
14  * If any of the files related to licensing are missing or if you have any
15  * other questions related to licensing please contact Breach Security, Inc.
16  * directly using the email address support@breach.com.
17  *
18  */
19 #include "modsecurity.h"
20 #include "apache2.h"
21 #include "pdf_protect.h"
22
23 #include <ctype.h>
24 #include "apr_sha1.h"
25
26 #define DEFAULT_TIMEOUT         10
27 #define DEFAULT_TOKEN_NAME      "PDFPTOKEN"
28 #define ATTACHMENT_MIME_TYPE    "application/octet-stream"
29 #define NOTE_TWEAK_HEADERS      "pdfp-note-tweakheaders"
30 #define NOTE_DONE               "pdfp-note-done"
31 #define REDIRECT_STATUS         HTTP_TEMPORARY_REDIRECT
32 #define DISPOSITION_VALUE       "attachment;"
33
34 // TODO We need ID and REV values for the PDF XSS alert.
35
36 // TODO It would be nice if the user could choose the ID/REV/SEVERITY/MESSAGE, etc.
37
38 static char *encode_sha1_base64(apr_pool_t *mp, const char *data) {
39     unsigned char digest[APR_SHA1_DIGESTSIZE];
40     apr_sha1_ctx_t context;
41
42     /* Calculate the hash first. */
43     apr_sha1_init(&context);
44     apr_sha1_update(&context, data, strlen(data));
45     apr_sha1_final(digest, &context);
46
47     /* Now transform with transport-friendly hex encoding. */
48     return bytes2hex(mp, digest, APR_SHA1_DIGESTSIZE);
49 }
50
51 static char *create_hash(modsec_rec *msr,
52     const char *time_string)
53 {
54     const char *content = NULL;
55
56     if (msr->txcfg->pdfp_secret == NULL) {
57         msr_log(msr, 1, "PdfProtect: Unable to generate hash. Please configure SecPdfProtectSecret.");
58         return NULL;
59     }
60
61     /* Our protection token is made out of the client's IP
62      * address, the secret key, and the token expiry time.
63      */
64     content = apr_pstrcat(msr->mp, msr->remote_addr, msr->txcfg->pdfp_secret,
65         time_string, NULL);
66     if (content == NULL) return NULL;
67
68     return encode_sha1_base64(msr->mp, content);
69 }
70
71 /**
72  *
73  */
74 static char *create_token(modsec_rec *msr) {
75     apr_time_t current_time;
76     const char *time_string = NULL;
77     const char *hash = NULL;
78     int timeout = DEFAULT_TIMEOUT;
79
80     if (msr->txcfg->pdfp_timeout != -1) {
81         timeout = msr->txcfg->pdfp_timeout;
82     }
83
84     current_time = apr_time_sec(apr_time_now());
85     time_string = apr_psprintf(msr->mp, "%" APR_TIME_T_FMT, (apr_time_t)(current_time + timeout));
86     if (time_string == NULL) return NULL;
87
88     hash = create_hash(msr, time_string);
89     if (hash == NULL) return NULL;
90
91     return apr_pstrcat(msr->mp, hash, "|", time_string, NULL);
92 }
93
94 /**
95  *
96  */
97 static char *construct_new_uri(modsec_rec *msr) {
98     const char *token_parameter = NULL;
99     const char *new_uri = NULL;
100     const char *token = NULL;
101     const char *token_name = DEFAULT_TOKEN_NAME;
102
103     token = create_token(msr);
104     if (token == NULL) return NULL;
105
106     if (msr->txcfg->pdfp_token_name != NULL) {
107         token_name = msr->txcfg->pdfp_token_name;
108     }
109
110     token_parameter = apr_pstrcat(msr->mp, token_name, "=", token, NULL);
111     if (token_parameter == NULL) return NULL;
112
113     if (msr->r->args == NULL) { /* No other parameters. */
114         new_uri = apr_pstrcat(msr->mp, msr->r->uri, "?", token_parameter, "#PDFP", NULL);
115     } else { /* Preserve existing paramters. */
116         new_uri = apr_pstrcat(msr->mp, msr->r->uri, "?", msr->r->args, "&",
117             token_parameter, "#PDFP", NULL);
118     }
119
120     return (char *)new_uri;
121 }
122
123 /**
124  *
125  */
126 static char *extract_token(modsec_rec *msr) {
127     char *search_string = NULL;
128     char *p = NULL, *t = NULL;
129     const char *token_name = DEFAULT_TOKEN_NAME;
130
131     if ((msr->r == NULL)||(msr->r->args == NULL)) {
132         return NULL;
133     }
134
135     if (msr->txcfg->pdfp_token_name != NULL) {
136         token_name = msr->txcfg->pdfp_token_name;
137     }
138
139     search_string = apr_pstrcat(msr->mp, msr->txcfg->pdfp_token_name, "=", NULL);
140     if (search_string == NULL) return NULL;
141
142     p = strstr(msr->r->args, search_string);
143     if (p == NULL) return NULL;
144
145     t = p = p + strlen(search_string);
146     while ((*t != '\0')&&(*t != '&')) t++;
147
148     return apr_pstrmemdup(msr->mp, p, t - p);
149 }
150
151 /**
152  *
153  */
154 static int validate_time_string(const char *time_string) {
155     char *p = (char *)time_string;
156
157     while(*p != '\0') {
158         if (!isdigit(*p)) return 0;
159         p++;
160     }
161
162     return 1;
163 }
164
165 /**
166  *
167  */
168 static int verify_token(modsec_rec *msr, const char *token, char **error_msg) {
169     unsigned int current_time, expiry_time;
170     const char *time_string = NULL;
171     const char *given_hash = NULL;
172     const char *hash = NULL;
173     const char *p = NULL;
174
175     if (error_msg == NULL) return 0;
176     *error_msg = NULL;
177
178     /* Split token into its parts - hash and expiry time. */
179     p = strstr(token, "|");
180     if (p == NULL) return 0;
181
182     given_hash = apr_pstrmemdup(msr->mp, token, p - token);
183     time_string = p + 1;
184     if (!validate_time_string(time_string)) {
185         *error_msg = apr_psprintf(msr->mp, "PdfProtect: Invalid time string: %s",
186             log_escape_nq(msr->mp, time_string));
187         return 0;
188     }
189     expiry_time = atoi(time_string);
190
191     /* Check the hash. */
192     hash = create_hash(msr, time_string);
193     if (strcmp(given_hash, hash) != 0) {
194         *error_msg = apr_psprintf(msr->mp, "PdfProtect: Invalid hash: %s (expected %s)",
195             log_escape_nq(msr->mp, given_hash), log_escape_nq(msr->mp, hash));
196         return 0;
197     }
198
199     /* Check time. */
200     current_time = apr_time_sec(apr_time_now());
201     if (current_time > expiry_time) {
202         *error_msg = apr_psprintf(msr->mp, "PdfProtect: Token has expired.");
203         return 0;
204     }
205
206     return 1;
207 }
208
209 /**
210  *
211  */
212 apr_status_t pdfp_output_filter(ap_filter_t *f, apr_bucket_brigade *bb_in) {
213     modsec_rec *msr = (modsec_rec *)f->ctx;
214
215     if (msr == NULL) {
216         ap_log_error(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, 0, f->r->server,
217             "ModSecurity: Internal Error: Unable to retrieve context in PDF output filter.");
218
219         ap_remove_output_filter(f);
220
221         return send_error_bucket(msr, f, HTTP_INTERNAL_SERVER_ERROR);
222     }
223
224     if (msr->txcfg->pdfp_enabled == 1) {
225         // TODO Should we look at err_headers_out too?
226         const char *h_content_type = apr_table_get(f->r->headers_out, "Content-Type");
227
228         if (msr->txcfg->debuglog_level >= 9) {
229             msr_log(msr, 9, "PdfProtect: r->content_type=%s, header C-T=%s",
230                 log_escape_nq(msr->mp, f->r->content_type),
231                 log_escape_nq(msr->mp, h_content_type));
232         }
233
234         /* Have we been asked to tweak the headers? */
235         if (apr_table_get(f->r->notes, NOTE_TWEAK_HEADERS) != NULL) {
236             /* Yes! */
237             apr_table_set(f->r->headers_out, "Content-Disposition", DISPOSITION_VALUE);
238             f->r->content_type = ATTACHMENT_MIME_TYPE;
239         }
240
241         /* Check if we've already done the necessary work in
242          * the first phase.
243          */
244         if (apr_table_get(f->r->notes, NOTE_DONE) != NULL) {
245             /* We have, so return straight away. */
246             ap_remove_output_filter(f);
247             return ap_pass_brigade(f->next, bb_in);
248         }
249
250         /* Proceed to detect dynamically-generated PDF files. */
251
252         // TODO application/x-pdf, application/vnd.fdf, application/vnd.adobe.xfdf,
253         // application/vnd.adobe.xdp+xml, application/vnd.adobe.xfd+xml, application/vnd.pdf
254         // application/acrobat, text/pdf, text/x-pdf ???
255         if (((f->r->content_type != NULL)&&(strcasecmp(f->r->content_type, "application/pdf") == 0))
256             || ((h_content_type != NULL)&&(strcasecmp(h_content_type, "application/pdf") == 0)))
257         {
258             request_rec *r = f->r;
259             const char *token = NULL;
260
261             if (msr->txcfg->debuglog_level >= 9) {
262                 msr_log(msr, 9, "PdfProtect: Detected a dynamically-generated PDF in %s",
263                     log_escape_nq(msr->mp, r->uri));
264             }
265
266             /* If we are configured with ForcedDownload protection method then we
267              * can do our thing here and finish early.
268              */
269             if (msr->txcfg->pdfp_method == PDF_PROTECT_METHOD_FORCED_DOWNLOAD) {
270                 if (msr->txcfg->debuglog_level >= 9) {
271                     msr_log(msr, 9, "PdfProtect: Forcing download of a dynamically "
272                         "generated PDF file.");
273                 }
274
275                 apr_table_set(f->r->headers_out, "Content-Disposition", DISPOSITION_VALUE);
276                 f->r->content_type = ATTACHMENT_MIME_TYPE;
277
278                 ap_remove_output_filter(f);
279
280                 return ap_pass_brigade(f->next, bb_in);
281             }
282
283             /* If we are here that means TokenRedirection is the desired protection method. */
284
285             /* Is this a non-GET request? */
286             if ((f->r->method_number != M_GET)&&
287                 ((msr->txcfg->pdfp_only_get == 1)||(msr->txcfg->pdfp_only_get == -1))
288             ) {
289                 /* This is a non-GET request and we have been configured
290                  * not to intercept it. So we are going to tweak the headers
291                  * to force download.
292                  */
293                 if (msr->txcfg->debuglog_level >= 9) {
294                     msr_log(msr, 9, "PdfProtect: Forcing download of a dynamically "
295                         "generated PDF file and non-GET method.");
296                 }
297
298                 apr_table_set(f->r->headers_out, "Content-Disposition", DISPOSITION_VALUE);
299                 f->r->content_type = ATTACHMENT_MIME_TYPE;
300
301                 ap_remove_output_filter(f);
302
303                 return ap_pass_brigade(f->next, bb_in);
304             }
305
306             /* Locate the protection token. */
307             token = extract_token(msr);
308
309             if (token == NULL) { /* No token. */
310                 char *new_uri = NULL;
311
312                 /* Create a new URI with the protection token inside. */
313                 new_uri = construct_new_uri(msr);
314                 if (new_uri != NULL) {
315                     /* Redirect user to the new URI. */
316                     if (msr->txcfg->debuglog_level >= 9) {
317                         msr_log(msr, 9, "PdfProtect: PDF request without a token - "
318                             "redirecting to %s.", log_escape_nq(msr->mp, new_uri));
319                     }
320
321                     apr_table_set(r->headers_out, "Location", new_uri);
322
323                     ap_remove_output_filter(f);
324
325                     return send_error_bucket(msr, f, REDIRECT_STATUS);
326                 }
327             } else { /* Token found. */
328                 char *my_error_msg = NULL;
329
330                 /* Verify the token is valid. */
331
332                 if (verify_token(msr, token, &my_error_msg)) { /* Valid. */
333                     /* Do nothing - serve the PDF file. */
334                     if (msr->txcfg->debuglog_level >= 9) {
335                         msr_log(msr, 9, "PdfProtect: PDF request with a valid token "
336                             "- serving PDF file normally.");
337                     }
338
339                     /* Fall through. */
340                 } else { /* Not valid. */
341                     /* The token is not valid. We will tweak the response
342                      * to prevent the PDF file from opening in the browser.
343                      */
344                     if (msr->txcfg->debuglog_level >= 4) {
345                         msr_log(msr, 9, "PdfProtect: PDF request with an invalid token "
346                             "- serving file as attachment.");
347                     }
348
349                     apr_table_set(r->headers_out, "Content-Disposition", DISPOSITION_VALUE);
350                     r->content_type = ATTACHMENT_MIME_TYPE;
351
352                     /* Fall through. */
353                 }
354             }
355         }
356     }
357
358     ap_remove_output_filter(f);
359
360     return ap_pass_brigade(f->next, bb_in);
361 }
362
363 /**
364  *
365  */
366 int pdfp_check(modsec_rec *msr) {
367     const char *token = NULL;
368     char *uri = NULL;
369     char *p = NULL;
370
371     if (msr->txcfg->pdfp_enabled != 1) {
372         if (msr->txcfg->debuglog_level >= 4) {
373             msr_log(msr, 4, "PdfProtect: Not enabled here.");
374         }
375
376         return 0;
377     }
378
379     if (msr->txcfg->pdfp_method != PDF_PROTECT_METHOD_TOKEN_REDIRECTION) {
380         if (msr->txcfg->debuglog_level >= 4) {
381             msr_log(msr, 4, "PdfProtect: Configured with ForcedDownload as protection method, "
382                 "skipping detection on the inbound.");
383         }
384
385         return 0;
386     }
387
388     /* Then determine whether we need to act at
389      * all. If the request is not for a PDF file
390      * return straight away.
391      */
392
393     if (msr->r->uri == NULL) {
394         if (msr->txcfg->debuglog_level >= 4) {
395             msr_log(msr, 4, "PdfProtect: Unable to inspect URI because it is NULL.");
396         }
397
398         return -1; /* Error. */
399     }
400
401     if (msr->txcfg->debuglog_level >= 9) {
402         msr_log(msr, 9, "PdfProtect: URI=%s, filename=%s, QUERY_STRING=%s.",
403             log_escape_nq(msr->mp, msr->r->uri), log_escape_nq(msr->mp, msr->r->filename),
404             log_escape_nq(msr->mp, msr->r->args));
405     }
406
407     uri = apr_pstrdup(msr->mp, msr->r->uri);
408     if (uri == NULL) return -1; /* Error. */
409     ap_str_tolower(uri);
410
411     /* Attempt to figure out if this is a request for a PDF file. We are
412      * going to be liberal and look for the extension anywhere in the URI,
413      * not just at the end.
414      */
415     p = strstr(uri, ".pdf");
416     if (p == NULL) {
417         /* We do not think this is a PDF file. */
418         if (msr->txcfg->debuglog_level >= 4) {
419             msr_log(msr, 4,  "PdfProtect: No indication in the URI this is a "
420                 "request for a PDF file.");
421         }
422
423         return 0;
424     }
425
426     /* Ignore request methods other than GET and HEAD if
427      * configured to do so.
428      *
429      * TODO: Code here is only GET, not GET|HEAD as comment states
430      */
431     if ((msr->r->method_number != M_GET)&&(msr->txcfg->pdfp_only_get != 0)) {
432         if (msr->txcfg->debuglog_level >= 4) {
433             msr_log(msr, 4, "PdfProtect: Not intercepting request "
434             "(method=%s/%d).", log_escape_nq(msr->mp, msr->r->method), msr->r->method_number);
435         }
436
437         return 0;
438     }
439
440     /* We make a note for ourselves that we've already handled
441      * the request.
442      */
443     apr_table_set(msr->r->notes, NOTE_DONE, "1");
444
445     /* Locate the protection token. */
446     token = extract_token(msr);
447
448     if (token == NULL) { /* No token. */
449         char *new_uri = NULL;
450
451         /* Create a new URI with the protection token inside. */
452         new_uri = construct_new_uri(msr);
453         if (new_uri == NULL) return DECLINED;
454
455         /* Redirect user to the new URI. */
456         if (msr->txcfg->debuglog_level >= 9) {
457             msr_log(msr, 9, "PdfProtect: PDF request without a token - redirecting to %s.",
458                 log_escape_nq(msr->mp, new_uri));
459         }
460
461         apr_table_set(msr->r->headers_out, "Location", new_uri);
462
463         return REDIRECT_STATUS;
464     } else { /* Token found. */
465         char *my_error_msg = NULL;
466
467         /* Verify the token is valid. */
468         if (verify_token(msr, token, &my_error_msg)) { /* Valid. */
469             /* Do nothing - serve the PDF file. */
470             if (msr->txcfg->debuglog_level >= 9) {
471                 msr_log(msr, 9, "PdfProtect: PDF request with a valid token - "
472                     "serving PDF file normally.");
473             }
474
475             return 0;
476         } else { /* Not valid. */
477             /* The token is not valid. We will tweak the response
478              * to prevent the PDF file from opening in the browser.
479              */
480
481             // TODO Log alert
482
483             /* Changing response headers before response generation phase takes
484              * place is not really reliable. Although we do this we also make
485              * a note for ourselves (in the output filter) to check the headers
486              * again just before they are sent back to the end user.
487              */
488             apr_table_set(msr->r->headers_out, "Content-Disposition", DISPOSITION_VALUE);
489             msr->r->content_type = ATTACHMENT_MIME_TYPE;
490             apr_table_set(msr->r->notes, NOTE_TWEAK_HEADERS, "1");
491
492             /* Proceed with response (PDF) generation. */
493             return 0;
494         }
495     }
496
497     return 0;
498 }