| 1 | /* |
|---|
| 2 | $ perldoc mod_checksum_filter.c |
|---|
| 3 | |
|---|
| 4 | =encoding utf8 |
|---|
| 5 | |
|---|
| 6 | =head1 NAME |
|---|
| 7 | |
|---|
| 8 | checksum_filter_module - アプリケーションとリバースプロキシ間での改竄検出フィルタ |
|---|
| 9 | |
|---|
| 10 | =head1 SYNOPSIS |
|---|
| 11 | |
|---|
| 12 | FilterDeclare checksum CONTENT_SET |
|---|
| 13 | FilterProvider checksum CHECKSUM Content-Type /^text\x2F/ |
|---|
| 14 | FilterChain checksum |
|---|
| 15 | RewriteEngine on |
|---|
| 16 | RewriteRule ^/proxy http://localhost:1978 [P] |
|---|
| 17 | |
|---|
| 18 | 特定のPATHにだけフィルタをかけたい場合 |
|---|
| 19 | |
|---|
| 20 | <Location /proxy> |
|---|
| 21 | FilterDeclare checksum CONTENT_SET |
|---|
| 22 | FilterProvider checksum CHECKSUM Content-Type /^text\x2F/ |
|---|
| 23 | FilterChain checksum |
|---|
| 24 | </Location> |
|---|
| 25 | RewriteEngine on |
|---|
| 26 | RewriteRule ^/proxy http://localhost:1978 [P] |
|---|
| 27 | |
|---|
| 28 | =head1 INSTALL |
|---|
| 29 | |
|---|
| 30 | shell# apxs -a -c -i mod_checksum_filter.c |
|---|
| 31 | |
|---|
| 32 | =head1 DESCRIPTION |
|---|
| 33 | |
|---|
| 34 | アプリケーションとリバースプロキシ間でのコンテンツ改竄を検出する用途として作成されています。 |
|---|
| 35 | このモジュールの影響下に有るURIではstatic fileへのリクエストだろうが何であろうが |
|---|
| 36 | X-CheckSum-ResponseとX-CheckSum-Requestがレスポンスヘッダに含まれていなければ |
|---|
| 37 | 502 Bad Gateway エラーを出力します。 |
|---|
| 38 | |
|---|
| 39 | アプリケーション側で特定のレスポンスヘッダを出力する事により検出を可能としていて、以下の二つのヘッダを出力する必要があります。 |
|---|
| 40 | |
|---|
| 41 | =head2 X-CheckSum-Response |
|---|
| 42 | |
|---|
| 43 | コンテンツ本体の改竄を検出出来ます。 |
|---|
| 44 | |
|---|
| 45 | Perl + HTTP::Engine 環境下では以下のようなコードで作成可能です。 |
|---|
| 46 | |
|---|
| 47 | my $body = '<html>.....</html>'; |
|---|
| 48 | $c->res->header( 'X-CheckSum-Response' => '{SHA}'.sha1_base64($body) ); |
|---|
| 49 | |
|---|
| 50 | =head2 X-CheckSum-Request |
|---|
| 51 | |
|---|
| 52 | 意図しないクライアントに間違ったレスポンスを送ってしまう事を検出出来ます。 |
|---|
| 53 | |
|---|
| 54 | Perl + HTTP::Engine 環境下では以下のようなコードで作成可能です。 |
|---|
| 55 | |
|---|
| 56 | my $request_data = sprintf '%s/%s', $c->req->address, $c->req->header('user-agent'); |
|---|
| 57 | $c->res->header( 'X-CheckSum-Request' => '{SHA}'.sha1_base64($request_data) ); |
|---|
| 58 | |
|---|
| 59 | =head1 ERROR DETECTION |
|---|
| 60 | |
|---|
| 61 | チェックサムに異常が見られた場合には error_log に以下のメッセージを出力します |
|---|
| 62 | |
|---|
| 63 | mod_checksum_filter: not checksum request |
|---|
| 64 | mod_checksum_filter: not checksum response |
|---|
| 65 | mod_checksum_filter: bad checksum request [sha1] != [sha1] |
|---|
| 66 | mod_checksum_filter: bad checksum response [sha1] != [sha1] |
|---|
| 67 | |
|---|
| 68 | =head1 TESTING |
|---|
| 69 | |
|---|
| 70 | 以下の Perl コードを立ち上げて mod_rewrite かなんかで飛ばしてテストすると良いよ |
|---|
| 71 | |
|---|
| 72 | use strict; |
|---|
| 73 | use warnings; |
|---|
| 74 | use Digest::SHA1 qw( sha1_base64 ); |
|---|
| 75 | use String::TT qw( tt strip ); |
|---|
| 76 | use HTTPEx::Declare; |
|---|
| 77 | |
|---|
| 78 | interface ServerSimple => {}; |
|---|
| 79 | run { |
|---|
| 80 | my $c = shift; |
|---|
| 81 | |
|---|
| 82 | my $body = tt strip q{ |
|---|
| 83 | <html> |
|---|
| 84 | <head> |
|---|
| 85 | <title>test</title> |
|---|
| 86 | </head> |
|---|
| 87 | <body> |
|---|
| 88 | test test |
|---|
| 89 | </body> |
|---|
| 90 | </html> |
|---|
| 91 | }; |
|---|
| 92 | |
|---|
| 93 | my $remote_ip = $c->req->param('ip') ? '192.168.192.168' : $c->req->address; |
|---|
| 94 | my $request_data = sprintf '%s/%s', $remote_ip, $c->req->header('user-agent'); |
|---|
| 95 | $c->res->header( 'X-CheckSum-Request' => '{SHA}'.sha1_base64($request_data) ); |
|---|
| 96 | $c->res->header( 'X-CheckSum-Response' => '{SHA}'.sha1_base64($body) ); |
|---|
| 97 | |
|---|
| 98 | $body .= 'bug' if $c->req->param('error'); |
|---|
| 99 | $c->res->body($body); |
|---|
| 100 | }; |
|---|
| 101 | |
|---|
| 102 | URI に ?error=1 や ?ip=1 をくっ付けると意図的にエラーを作り出せるよ |
|---|
| 103 | |
|---|
| 104 | =head1 SEE ALSO |
|---|
| 105 | |
|---|
| 106 | L<http://d.hatena.ne.jp/dayflower/20070416/1176705134> |
|---|
| 107 | |
|---|
| 108 | =head1 AUTHOR |
|---|
| 109 | |
|---|
| 110 | Kazuhiro Osawa |
|---|
| 111 | |
|---|
| 112 | =head1 LICENSE |
|---|
| 113 | |
|---|
| 114 | Licensed to the Apache Software Foundation (ASF) under one or more |
|---|
| 115 | contributor license agreements. See the NOTICE file distributed with |
|---|
| 116 | this work for additional information regarding copyright ownership. |
|---|
| 117 | The ASF licenses this file to You under the Apache License, Version 2.0 |
|---|
| 118 | (the "License"); you may not use this file except in compliance with |
|---|
| 119 | the License. You may obtain a copy of the License at |
|---|
| 120 | |
|---|
| 121 | http://www.apache.org/licenses/LICENSE-2.0 |
|---|
| 122 | |
|---|
| 123 | Unless required by applicable law or agreed to in writing, software |
|---|
| 124 | distributed under the License is distributed on an "AS IS" BASIS, |
|---|
| 125 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|---|
| 126 | See the License for the specific language governing permissions and |
|---|
| 127 | limitations under the License. |
|---|
| 128 | |
|---|
| 129 | =cut |
|---|
| 130 | |
|---|
| 131 | */ |
|---|
| 132 | |
|---|
| 133 | #include "httpd.h" |
|---|
| 134 | #include "http_config.h" |
|---|
| 135 | #include "http_log.h" |
|---|
| 136 | #include "util_filter.h" |
|---|
| 137 | |
|---|
| 138 | #include "apr_strings.h" |
|---|
| 139 | #include "apr_sha1.h" |
|---|
| 140 | |
|---|
| 141 | static const char checksumFilterName[] = "CHECKSUM"; |
|---|
| 142 | module AP_MODULE_DECLARE_DATA checksum_filter_module; |
|---|
| 143 | |
|---|
| 144 | typedef struct { |
|---|
| 145 | int eos; |
|---|
| 146 | apr_bucket_brigade *bb; |
|---|
| 147 | } checksum_ctx_t; |
|---|
| 148 | |
|---|
| 149 | /* |
|---|
| 150 | なんかエラーがあったら Bad Gateway レスポンスを全部リセットしちゃうよ |
|---|
| 151 | */ |
|---|
| 152 | static apr_status_t create_error_brigade(ap_filter_t *f, |
|---|
| 153 | apr_bucket_brigade *bb) |
|---|
| 154 | { |
|---|
| 155 | request_rec *r = f->r; |
|---|
| 156 | conn_rec *c = r->connection; |
|---|
| 157 | apr_bucket_brigade *out_bb; |
|---|
| 158 | apr_bucket *b; |
|---|
| 159 | |
|---|
| 160 | apr_brigade_destroy(bb); |
|---|
| 161 | |
|---|
| 162 | out_bb = apr_brigade_create(c->pool, c->bucket_alloc); |
|---|
| 163 | |
|---|
| 164 | b = ap_bucket_error_create(HTTP_BAD_GATEWAY, NULL, c->pool, c->bucket_alloc); |
|---|
| 165 | APR_BRIGADE_INSERT_TAIL(out_bb, b); |
|---|
| 166 | |
|---|
| 167 | b = apr_bucket_eos_create(c->bucket_alloc); |
|---|
| 168 | APR_BRIGADE_INSERT_TAIL(out_bb, b); |
|---|
| 169 | |
|---|
| 170 | r->no_cache = 1; |
|---|
| 171 | r->status = HTTP_BAD_GATEWAY; |
|---|
| 172 | r->status_line = apr_psprintf(r->pool, "502 Bad Gateway"); |
|---|
| 173 | |
|---|
| 174 | return ap_pass_brigade(f->next, out_bb); |
|---|
| 175 | } |
|---|
| 176 | |
|---|
| 177 | /* |
|---|
| 178 | apr_sha1_base64 で作った文字列の末尾にある = を取り除くよ |
|---|
| 179 | */ |
|---|
| 180 | static void strip_last_equal(char *s) |
|---|
| 181 | { |
|---|
| 182 | apr_size_t len; |
|---|
| 183 | char *sp; |
|---|
| 184 | if (!s) return; |
|---|
| 185 | |
|---|
| 186 | len = strlen(s); |
|---|
| 187 | if (len < 1) return; |
|---|
| 188 | |
|---|
| 189 | for (sp = s + len - 1;s != sp;sp--) { |
|---|
| 190 | if (*sp != '=') break; |
|---|
| 191 | *sp = '\0'; |
|---|
| 192 | } |
|---|
| 193 | } |
|---|
| 194 | |
|---|
| 195 | /* |
|---|
| 196 | reverse proxy 経由だろうが local file だろうが何でもチェックするよ |
|---|
| 197 | */ |
|---|
| 198 | static apr_status_t checksum_out_filter(ap_filter_t *f, |
|---|
| 199 | apr_bucket_brigade *bb) |
|---|
| 200 | { |
|---|
| 201 | request_rec *r = f->r; |
|---|
| 202 | checksum_ctx_t *ctx = f->ctx; |
|---|
| 203 | apr_bucket *b; |
|---|
| 204 | apr_bucket_brigade *out_bb; |
|---|
| 205 | char *content_body; |
|---|
| 206 | apr_size_t content_length; |
|---|
| 207 | apr_status_t rv; |
|---|
| 208 | char *request_data; |
|---|
| 209 | const char *checksum_request; |
|---|
| 210 | const char *checksum_response; |
|---|
| 211 | unsigned char request_sha1[120]; |
|---|
| 212 | unsigned char response_sha1[120]; |
|---|
| 213 | |
|---|
| 214 | /* Do nothing if asked to filter nothing. */ |
|---|
| 215 | if (APR_BRIGADE_EMPTY(bb)) { |
|---|
| 216 | return ap_pass_brigade(f->next, bb); |
|---|
| 217 | } |
|---|
| 218 | |
|---|
| 219 | /* already done */ |
|---|
| 220 | if (ctx && ctx->eos) { |
|---|
| 221 | return ap_pass_brigade(f->next, bb); |
|---|
| 222 | } |
|---|
| 223 | |
|---|
| 224 | if (!ctx) { |
|---|
| 225 | ctx = apr_palloc(r->pool, sizeof(checksum_ctx_t)); |
|---|
| 226 | f->ctx = ctx; |
|---|
| 227 | ctx->eos = 0; |
|---|
| 228 | ctx->bb = apr_brigade_create(r->pool, f->c->bucket_alloc); |
|---|
| 229 | } |
|---|
| 230 | |
|---|
| 231 | /* check the all brigades */ |
|---|
| 232 | b = APR_BRIGADE_FIRST(bb); |
|---|
| 233 | while (b != APR_BRIGADE_SENTINEL(bb)) { |
|---|
| 234 | apr_bucket *next_b = APR_BUCKET_NEXT(b); |
|---|
| 235 | |
|---|
| 236 | APR_BUCKET_REMOVE(b); |
|---|
| 237 | APR_BRIGADE_INSERT_TAIL(ctx->bb, b); |
|---|
| 238 | |
|---|
| 239 | if (APR_BUCKET_IS_EOS(b)) { |
|---|
| 240 | ctx->eos = 1; |
|---|
| 241 | } |
|---|
| 242 | |
|---|
| 243 | b = next_b; |
|---|
| 244 | } |
|---|
| 245 | |
|---|
| 246 | /* given brigade to trash */ |
|---|
| 247 | apr_brigade_destroy(bb); |
|---|
| 248 | |
|---|
| 249 | if (!ctx->eos) { |
|---|
| 250 | return APR_SUCCESS; |
|---|
| 251 | } |
|---|
| 252 | |
|---|
| 253 | /* reading stream completed */ |
|---|
| 254 | |
|---|
| 255 | rv = apr_brigade_pflatten(ctx->bb, &content_body, &content_length, r->pool); |
|---|
| 256 | if (rv != APR_SUCCESS) { |
|---|
| 257 | ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, |
|---|
| 258 | "mod_checksum_filter: apr_brigade_pflatten() failed"); |
|---|
| 259 | return create_error_brigade(f, ctx->bb); |
|---|
| 260 | } |
|---|
| 261 | |
|---|
| 262 | /* check request env */ |
|---|
| 263 | checksum_request = apr_table_get(r->headers_out, "X-CheckSum-Request"); |
|---|
| 264 | if (!checksum_request) { |
|---|
| 265 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, |
|---|
| 266 | "mod_checksum_filter: not checksum request (%dbytes)", content_length); |
|---|
| 267 | return create_error_brigade(f, ctx->bb); |
|---|
| 268 | } |
|---|
| 269 | apr_table_unset(r->headers_out, "X-CheckSum-Request"); |
|---|
| 270 | |
|---|
| 271 | request_data = apr_pstrcat(r->pool, |
|---|
| 272 | r->connection->remote_ip, "/", |
|---|
| 273 | apr_table_get(r->headers_in, "User-Agent"), NULL); |
|---|
| 274 | if (!request_data) { |
|---|
| 275 | ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, |
|---|
| 276 | "mod_checksum_filter: apr_pstrcat() failed"); |
|---|
| 277 | return create_error_brigade(f, ctx->bb); |
|---|
| 278 | } |
|---|
| 279 | apr_sha1_base64(request_data, strlen(request_data), request_sha1); |
|---|
| 280 | strip_last_equal(request_sha1); |
|---|
| 281 | |
|---|
| 282 | if (apr_strnatcmp(checksum_request, request_sha1) != 0) { |
|---|
| 283 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, |
|---|
| 284 | "mod_checksum_filter: bad checksum request %s != %s (%dbytes)", |
|---|
| 285 | checksum_request, request_sha1, content_length); |
|---|
| 286 | return create_error_brigade(f, ctx->bb); |
|---|
| 287 | } |
|---|
| 288 | |
|---|
| 289 | /* check response body */ |
|---|
| 290 | checksum_response = apr_table_get(r->headers_out, "X-CheckSum-Response"); |
|---|
| 291 | if (!checksum_response) { |
|---|
| 292 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, |
|---|
| 293 | "mod_checksum_filter: not checksum response (%dbytes)", content_length); |
|---|
| 294 | return create_error_brigade(f, ctx->bb); |
|---|
| 295 | } |
|---|
| 296 | apr_table_unset(r->headers_out, "X-CheckSum-Response"); |
|---|
| 297 | |
|---|
| 298 | apr_sha1_base64(content_body, content_length, response_sha1); |
|---|
| 299 | strip_last_equal(response_sha1); |
|---|
| 300 | |
|---|
| 301 | if (apr_strnatcmp(checksum_response, response_sha1) != 0) { |
|---|
| 302 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, |
|---|
| 303 | "mod_checksum_filter: bad checksum response %s != %s (%dbytes)", |
|---|
| 304 | checksum_response, response_sha1, content_length); |
|---|
| 305 | return create_error_brigade(f, ctx->bb); |
|---|
| 306 | } |
|---|
| 307 | |
|---|
| 308 | return ap_pass_brigade(f->next, ctx->bb); |
|---|
| 309 | } |
|---|
| 310 | |
|---|
| 311 | static const command_rec checksum_filter_cmds[] = |
|---|
| 312 | { |
|---|
| 313 | {NULL} |
|---|
| 314 | }; |
|---|
| 315 | |
|---|
| 316 | static void register_hooks(apr_pool_t *p) |
|---|
| 317 | { |
|---|
| 318 | ap_register_output_filter(checksumFilterName, checksum_out_filter, NULL, |
|---|
| 319 | AP_FTYPE_CONTENT_SET); |
|---|
| 320 | } |
|---|
| 321 | |
|---|
| 322 | module AP_MODULE_DECLARE_DATA checksum_filter_module = |
|---|
| 323 | { |
|---|
| 324 | STANDARD20_MODULE_STUFF, |
|---|
| 325 | NULL, |
|---|
| 326 | NULL, |
|---|
| 327 | NULL, |
|---|
| 328 | NULL, |
|---|
| 329 | checksum_filter_cmds, |
|---|
| 330 | register_hooks |
|---|
| 331 | }; |
|---|