From a68eb048849d13aefea27a6d5fcac1e9b234fb56 Mon Sep 17 00:00:00 2001 From: brectanus Date: Fri, 11 May 2007 16:14:11 +0000 Subject: [PATCH] Add geo lookup support. See #22. --- CHANGES | 3 + apache2/Makefile.win | 3 +- apache2/apache2_config.c | 34 ++ apache2/modsecurity.c | 3 + apache2/modsecurity.h | 7 + apache2/modules.mk | 5 +- apache2/msc_geo.c | 447 ++++++++++++++++++++++++++ apache2/msc_geo.h | 62 ++++ apache2/msc_util.c | 12 + apache2/msc_util.h | 2 + apache2/re.h | 1 - apache2/re_operators.c | 104 ++++++ apache2/re_variables.c | 55 ++++ doc/modsecurity2-apache-reference.xml | 114 ++++++- 14 files changed, 847 insertions(+), 5 deletions(-) create mode 100644 apache2/msc_geo.c create mode 100644 apache2/msc_geo.h diff --git a/CHANGES b/CHANGES index df99f262..fc531217 100644 --- a/CHANGES +++ b/CHANGES @@ -2,6 +2,9 @@ ?? ??? 2007 - 2.2.0-trunk ------------------------- + * Add SecGeoLookupsDb, @geoLookups and GEO collection to support + geographical lookups by IP/host. + * Do not try to intercept a request after a failed rule. This fixes the issue associated with an "Internal Error: Asked to intercept request but was_intercepted is zero" error message. diff --git a/apache2/Makefile.win b/apache2/Makefile.win index 8691fb6b..071741c2 100644 --- a/apache2/Makefile.win +++ b/apache2/Makefile.win @@ -23,7 +23,8 @@ 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 pdf_protec.obj + msc_util.obj msc_pcre.obj persist_dbm.obj msc_reqbody.obj pdf_protect.obj \ + msc_geo.obj all: $(DLL) diff --git a/apache2/apache2_config.c b/apache2/apache2_config.c index c0c4d548..e19da940 100644 --- a/apache2/apache2_config.c +++ b/apache2/apache2_config.c @@ -91,6 +91,9 @@ void *create_directory_config(apr_pool_t *mp, char *path) { dcfg->pdfp_token_name = NOT_SET_P; dcfg->pdfp_only_get = NOT_SET; + /* Geo Lookups */ + dcfg->geo = NOT_SET_P; + return dcfg; } @@ -382,6 +385,10 @@ void *merge_directory_configs(apr_pool_t *mp, void *_parent, void *_child) { merged->pdfp_only_get = (child->pdfp_only_get == NOT_SET ? parent->pdfp_only_get : child->pdfp_only_get); + /* Geo Lookup */ + merged->geo = (child->geo == NOT_SET_P + ? parent->geo : child->geo); + return merged; } @@ -450,6 +457,9 @@ void init_directory_config(directory_config *dcfg) { 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; + + /* Geo Lookup */ + if (dcfg->geo == NOT_SET_P) dcfg->geo = NULL; } /** @@ -1185,6 +1195,22 @@ static const char *cmd_pdf_protect_intercept_get_only(cmd_parms *cmd, void *_dcf return NULL; } +/* -- Geo Lookup configuration -- */ + +static const char *cmd_geo_lookups_db(cmd_parms *cmd, void *_dcfg, + const char *p1) +{ + char *error_msg; + directory_config *dcfg = (directory_config *)_dcfg; + if (dcfg == NULL) return NULL; + + if (geo_init(dcfg, p1, &error_msg) <= 0) { + return error_msg; + } + + return NULL; +} + /* -- Configuration directives definitions -- */ @@ -1524,5 +1550,13 @@ const command_rec module_directives[] = { "whether or not to intercept only GET requess." ), + AP_INIT_TAKE1 ( + "SecGeoLookupsDb", + cmd_geo_lookups_db, + NULL, + RSRC_CONF, + "database for geographical lookups module." + ), + { NULL } }; diff --git a/apache2/modsecurity.c b/apache2/modsecurity.c index 8ae49b6d..e890bc1b 100644 --- a/apache2/modsecurity.c +++ b/apache2/modsecurity.c @@ -289,6 +289,9 @@ apr_status_t modsecurity_tx_init(modsec_rec *msr) { msr->tx_vars = apr_table_make(msr->mp, 32); if (msr->tx_vars == NULL) return -1; + msr->geo_vars = apr_table_make(msr->mp, 8); + if (msr->geo_vars == NULL) return -1; + msr->collections = apr_table_make(msr->mp, 8); if (msr->collections == NULL) return -1; msr->collections_dirty = apr_table_make(msr->mp, 8); diff --git a/apache2/modsecurity.h b/apache2/modsecurity.h index 37135172..4b6abe59 100644 --- a/apache2/modsecurity.h +++ b/apache2/modsecurity.h @@ -39,6 +39,7 @@ typedef struct msc_string msc_string; #ifdef WITH_LIBXML2 #include "msc_xml.h" #endif +#include "msc_geo.h" #include "re.h" #include "ap_config.h" @@ -243,6 +244,9 @@ struct modsec_rec { apr_table_t *tx_vars; + /* ENH: refactor to allow arbitrary var tables */ + apr_table_t *geo_vars; + /* response */ unsigned int response_status; const char *status_line; @@ -402,6 +406,9 @@ struct directory_config { int pdfp_timeout; const char *pdfp_token_name; int pdfp_only_get; + + /* Geo Lookup */ + geo_db *geo; }; struct error_message { diff --git a/apache2/modules.mk b/apache2/modules.mk index f8c97b94..2f9cfbb4 100644 --- a/apache2/modules.mk +++ b/apache2/modules.mk @@ -2,10 +2,11 @@ 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 pdf_protect + persist_dbm msc_reqbody pdf_protect msc_geo 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 pdf_protect.h + msc_pcre.h msc_util.h msc_xml.h persist_dbm.h apache2.h pdf_protect.h \ + msc_geo.h ${MOD_SECURITY2:=.slo}: ${H} ${MOD_SECURITY2:=.lo}: ${H} diff --git a/apache2/msc_geo.c b/apache2/msc_geo.c new file mode 100644 index 00000000..798d8d28 --- /dev/null +++ b/apache2/msc_geo.c @@ -0,0 +1,447 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * 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 "msc_geo.h" + + +/* -- Lookup Tables -- */ + +static const char *geo_country_code[] = { + "--", + "AP","EU","AD","AE","AF","AG","AI","AL","AM","AN", + "AO","AQ","AR","AS","AT","AU","AW","AZ","BA","BB", + "BD","BE","BF","BG","BH","BI","BJ","BM","BN","BO", + "BR","BS","BT","BV","BW","BY","BZ","CA","CC","CD", + "CF","CG","CH","CI","CK","CL","CM","CN","CO","CR", + "CU","CV","CX","CY","CZ","DE","DJ","DK","DM","DO", + "DZ","EC","EE","EG","EH","ER","ES","ET","FI","FJ", + "FK","FM","FO","FR","FX","GA","GB","GD","GE","GF", + "GH","GI","GL","GM","GN","GP","GQ","GR","GS","GT", + "GU","GW","GY","HK","HM","HN","HR","HT","HU","ID", + "IE","IL","IN","IO","IQ","IR","IS","IT","JM","JO", + "JP","KE","KG","KH","KI","KM","KN","KP","KR","KW", + "KY","KZ","LA","LB","LC","LI","LK","LR","LS","LT", + "LU","LV","LY","MA","MC","MD","MG","MH","MK","ML", + "MM","MN","MO","MP","MQ","MR","MS","MT","MU","MV", + "MW","MX","MY","MZ","NA","NC","NE","NF","NG","NI", + "NL","NO","NP","NR","NU","NZ","OM","PA","PE","PF", + "PG","PH","PK","PL","PM","PN","PR","PS","PT","PW", + "PY","QA","RE","RO","RU","RW","SA","SB","SC","SD", + "SE","SG","SH","SI","SJ","SK","SL","SM","SN","SO", + "SR","ST","SV","SY","SZ","TC","TD","TF","TG","TH", + "TJ","TK","TM","TN","TO","TL","TR","TT","TV","TW", + "TZ","UA","UG","UM","US","UY","UZ","VA","VC","VE", + "VG","VI","VN","VU","WF","WS","YE","YT","RS","ZA", + "ZM","ME","ZW","A1","A2","O1","AX","GG","IM","JE" +}; + +static const char *geo_country_code3[] = { + "--", + "AP","EU","AND","ARE","AFG","ATG","AIA","ALB","ARM","ANT", + "AGO","AQ","ARG","ASM","AUT","AUS","ABW","AZE","BIH","BRB", + "BGD","BEL","BFA","BGR","BHR","BDI","BEN","BMU","BRN","BOL", + "BRA","BHS","BTN","BV","BWA","BLR","BLZ","CAN","CC","COD", + "CAF","COG","CHE","CIV","COK","CHL","CMR","CHN","COL","CRI", + "CUB","CPV","CX","CYP","CZE","DEU","DJI","DNK","DMA","DOM", + "DZA","ECU","EST","EGY","ESH","ERI","ESP","ETH","FIN","FJI", + "FLK","FSM","FRO","FRA","FX","GAB","GBR","GRD","GEO","GUF", + "GHA","GIB","GRL","GMB","GIN","GLP","GNQ","GRC","GS","GTM", + "GUM","GNB","GUY","HKG","HM","HND","HRV","HTI","HUN","IDN", + "IRL","ISR","IND","IO","IRQ","IRN","ISL","ITA","JAM","JOR", + "JPN","KEN","KGZ","KHM","KIR","COM","KNA","PRK","KOR","KWT", + "CYM","KAZ","LAO","LBN","LCA","LIE","LKA","LBR","LSO","LTU", + "LUX","LVA","LBY","MAR","MCO","MDA","MDG","MHL","MKD","MLI", + "MMR","MNG","MAC","MNP","MTQ","MRT","MSR","MLT","MUS","MDV", + "MWI","MEX","MYS","MOZ","NAM","NCL","NER","NFK","NGA","NIC", + "NLD","NOR","NPL","NRU","NIU","NZL","OMN","PAN","PER","PYF", + "PNG","PHL","PAK","POL","SPM","PCN","PRI","PSE","PRT","PLW", + "PRY","QAT","REU","ROU","RUS","RWA","SAU","SLB","SYC","SDN", + "SWE","SGP","SHN","SVN","SJM","SVK","SLE","SMR","SEN","SOM", + "SUR","STP","SLV","SYR","SWZ","TCA","TCD","TF","TGO","THA", + "TJK","TKL","TKM","TUN","TON","TLS","TUR","TTO","TUV","TWN", + "TZA","UKR","UGA","UM","USA","URY","UZB","VAT","VCT","VEN", + "VGB","VIR","VNM","VUT","WLF","WSM","YEM","YT","SRB","ZAF", + "ZMB","MNE","ZWE","A1","A2","O1","ALA","GGY","IMN","JEY" +}; + +static const char *geo_country_name[] = { + "N/A", + "Asia/Pacific Region","Europe","Andorra","United Arab Emirates","Afghanistan","Antigua and Barbuda","Anguilla","Albania","Armenia","Netherlands Antilles", + "Angola","Antarctica","Argentina","American Samoa","Austria","Australia","Aruba","Azerbaijan","Bosnia and Herzegovina","Barbados", + "Bangladesh","Belgium","Burkina Faso","Bulgaria","Bahrain","Burundi","Benin","Bermuda","Brunei Darussalam","Bolivia", + "Brazil","Bahamas","Bhutan","Bouvet Island","Botswana","Belarus","Belize","Canada","Cocos (Keeling) Islands","Congo, The Democratic Republic of the", + "Central African Republic","Congo","Switzerland","Cote D'Ivoire","Cook Islands","Chile","Cameroon","China","Colombia","Costa Rica", + "Cuba","Cape Verde","Christmas Island","Cyprus","Czech Republic","Germany","Djibouti","Denmark","Dominica","Dominican Republic", + "Algeria","Ecuador","Estonia","Egypt","Western Sahara","Eritrea","Spain","Ethiopia","Finland","Fiji", + "Falkland Islands (Malvinas)","Micronesia, Federated States of","Faroe Islands","France","France, Metropolitan","Gabon","United Kingdom","Grenada","Georgia","French Guiana", + "Ghana","Gibraltar","Greenland","Gambia","Guinea","Guadeloupe","Equatorial Guinea","Greece","South Georgia and the South Sandwich Islands","Guatemala", + "Guam","Guinea-Bissau","Guyana","Hong Kong","Heard Island and McDonald Islands","Honduras","Croatia","Haiti","Hungary","Indonesia", + "Ireland","Israel","India","British Indian Ocean Territory","Iraq","Iran, Islamic Republic of","Iceland","Italy","Jamaica","Jordan", + "Japan","Kenya","Kyrgyzstan","Cambodia","Kiribati","Comoros","Saint Kitts and Nevis","Korea, Democratic People's Republic of","Korea, Republic of","Kuwait", + "Cayman Islands","Kazakhstan","Lao People's Democratic Republic","Lebanon","Saint Lucia","Liechtenstein","Sri Lanka","Liberia","Lesotho","Lithuania", + "Luxembourg","Latvia","Libyan Arab Jamahiriya","Morocco","Monaco","Moldova, Republic of","Madagascar","Marshall Islands","Macedonia","Mali", + "Myanmar","Mongolia","Macau","Northern Mariana Islands","Martinique","Mauritania","Montserrat","Malta","Mauritius","Maldives", + "Malawi","Mexico","Malaysia","Mozambique","Namibia","New Caledonia","Niger","Norfolk Island","Nigeria","Nicaragua", + "Netherlands","Norway","Nepal","Nauru","Niue","New Zealand","Oman","Panama","Peru","French Polynesia", + "Papua New Guinea","Philippines","Pakistan","Poland","Saint Pierre and Miquelon","Pitcairn Islands","Puerto Rico","Palestinian Territory","Portugal","Palau", + "Paraguay","Qatar","Reunion","Romania","Russian Federation","Rwanda","Saudi Arabia","Solomon Islands","Seychelles","Sudan", + "Sweden","Singapore","Saint Helena","Slovenia","Svalbard and Jan Mayen","Slovakia","Sierra Leone","San Marino","Senegal","Somalia","Suriname", + "Sao Tome and Principe","El Salvador","Syrian Arab Republic","Swaziland","Turks and Caicos Islands","Chad","French Southern Territories","Togo","Thailand", + "Tajikistan","Tokelau","Turkmenistan","Tunisia","Tonga","Timor-Leste","Turkey","Trinidad and Tobago","Tuvalu","Taiwan", + "Tanzania, United Republic of","Ukraine","Uganda","United States Minor Outlying Islands","United States","Uruguay","Uzbekistan","Holy See (Vatican City State)","Saint Vincent and the Grenadines","Venezuela", + "Virgin Islands, British","Virgin Islands, U.S.","Vietnam","Vanuatu","Wallis and Futuna","Samoa","Yemen","Mayotte","Serbia","South Africa", + "Zambia","Montenegro","Zimbabwe","Anonymous Proxy","Satellite Provider","Other","Aland Islands","Guernsey","Isle of Man","Jersey" +}; + +static const char *geo_country_continent[] = { + "--", + "AS","EU","EU","AS","AS","SA","SA","EU","AS","SA", + "AF","AN","SA","OC","EU","OC","SA","AS","EU","SA", + "AS","EU","AF","EU","AS","AF","AF","SA","AS","SA", + "SA","SA","AS","AF","AF","EU","SA","NA","AS","AF", + "AF","AF","EU","AF","OC","SA","AF","AS","SA","SA", + "SA","AF","AS","AS","EU","EU","AF","EU","SA","SA", + "AF","SA","EU","AF","AF","AF","EU","AF","EU","OC", + "SA","OC","EU","EU","EU","AF","EU","SA","AS","SA", + "AF","EU","SA","AF","AF","SA","AF","EU","SA","SA", + "OC","AF","SA","AS","AF","SA","EU","SA","EU","AS", + "EU","AS","AS","AS","AS","AS","EU","EU","SA","AS", + "AS","AF","AS","AS","OC","AF","SA","AS","AS","AS", + "SA","AS","AS","AS","SA","EU","AS","AF","AF","EU", + "EU","EU","AF","AF","EU","EU","AF","OC","EU","AF", + "AS","AS","AS","OC","SA","AF","SA","EU","AF","AS", + "AF","NA","AS","AF","AF","OC","AF","OC","AF","SA", + "EU","EU","AS","OC","OC","OC","AS","SA","SA","OC", + "OC","AS","AS","EU","SA","OC","SA","AS","EU","OC", + "SA","AS","AF","EU","AS","AF","AS","OC","AF","AF", + "EU","AS","AF","EU","EU","EU","AF","EU","AF","AF", + "SA","AF","SA","AS","AF","SA","AF","AF","AF","AS", + "AS","OC","AS","AF","OC","AS","AS","SA","OC","AS", + "AF","EU","AF","OC","NA","SA","AS","EU","SA","SA", + "SA","SA","AS","OC","OC","OC","AS","AF","EU","AF", + "AF","EU","AF","--","--","--","EU","EU","EU","EU" +}; + + +static int db_open(directory_config *dcfg, char **error_msg) +{ + char errstr[1024]; + apr_pool_t *mp = dcfg->mp; + geo_db *geo = dcfg->geo; + apr_status_t rc; + apr_size_t nbytes; + apr_off_t offset; + unsigned char buf[3]; + int i, j; + + #ifdef DEBUG_CONF + fprintf(stderr, "GEO: Initializing geo DB \"%s\".\n", geo->dbfn); + #endif + + if ((rc = apr_file_open(&geo->db, geo->dbfn, APR_READ, APR_OS_DEFAULT, mp)) != APR_SUCCESS) { + *error_msg = apr_psprintf(mp, "Could not open geo database \"%s\": %s", geo->dbfn, apr_strerror(rc, errstr, 1024)); + return 0; + } + + offset = -3; + apr_file_seek(geo->db, APR_END, &offset); + /* TODO check offset */ + + /* Defaults */ + geo->dbtype = GEO_COUNTRY_DATABASE; + geo->ctry_offset = GEO_COUNTRY_OFFSET; + + for (i = 0; i < GEO_STRUCT_INFO_MAX_SIZE; i++) { + memset(buf, 0, 3); + rc = apr_file_read_full(geo->db, &buf, 3, &nbytes); + #ifdef DEBUG_CONF + fprintf(stderr, "GEO: read 0x%02x%02x%02x\n", buf[0], buf[1], buf[2]); + #endif + if ((rc != APR_SUCCESS) || (nbytes != 3)) { + *error_msg = apr_psprintf(mp, "Could not read from geo database \"%s\" (%d/3 bytes read): %s", geo->dbfn, nbytes, apr_strerror(rc, errstr, 1024)); + return -1; + } + if ((buf[0] == 0xff) && (buf[1] == 0xff) && (buf[2] == 0xff)) { + #ifdef DEBUG_CONF + fprintf(stderr, "GEO: Found DB info marker at offset 0x%08x\n", (unsigned int)offset); + #endif + memset(buf, 0, 3); + rc = apr_file_read_full(geo->db, &buf, 1, &nbytes); + /* TODO: check rc */ + geo->dbtype = (int)buf[0]; + + /* Backwards compat */ + if (geo->dbtype >= 106) { + geo->dbtype -= 105; + } + #ifdef DEBUG_CONF + fprintf(stderr, "GEO: DB type %d\n", geo->dbtype); + #endif + + /* If a cities DB, then get country offset */ + if ((geo->dbtype == GEO_CITY_DATABASE_0) || (geo->dbtype == GEO_CITY_DATABASE_1)) { + memset(buf, 0, 3); + rc = apr_file_read_full(geo->db, &buf, 3, &nbytes); + if ((rc != APR_SUCCESS) || (nbytes != 3)) { + *error_msg = apr_psprintf(mp, "Could not read geo database \"%s\" country offset (%d/3 bytes read): %s", geo->dbfn, nbytes, apr_strerror(rc, errstr, 1024)); + return -1; + } + #ifdef DEBUG_CONF + fprintf(stderr, "GEO: read 0x%02x%02x%02x\n", buf[0], buf[1], buf[2]); + #endif + geo->ctry_offset = 0; + for (j = 0; j < 3; j++) { + geo->ctry_offset += (buf[j] << (j * 8)); + } + } + + #ifdef DEBUG_CONF + fprintf(stderr, "GEO: Country offset 0x%08x\n", geo->ctry_offset); + #endif + + return 1; + } + /* Backup a byte from where we started */ + offset = -4; + apr_file_seek(geo->db, APR_CUR, &offset); + #ifdef DEBUG_CONF + fprintf(stderr, "GEO: DB offset 0x%08x\n", (unsigned int)offset); + #endif + } + + if (geo->dbtype != GEO_COUNTRY_DATABASE) { + *error_msg = apr_psprintf(mp, "Unknown database format"); + return 0; + } + + #ifdef DEBUG_CONF + fprintf(stderr, "GEO: DB type %d\n", geo->dbtype); + #endif + + return 1; +} + +/** + * Initialise Geo data structure + */ +int geo_init(directory_config *dcfg, const char *dbfn, char **error_msg) +{ + *error_msg = NULL; + + if ((dcfg->geo == NULL) || (dcfg->geo == NOT_SET_P)) { + dcfg->geo = apr_pcalloc(dcfg->mp, sizeof(geo_db)); + } + + dcfg->geo->db = NULL; + dcfg->geo->dbfn = apr_pstrdup(dcfg->mp, dbfn); + dcfg->geo->dbtype = 0; + dcfg->geo->ctry_offset = 0; + + return db_open(dcfg, error_msg); +} + +/** + * Perform geographical lookup on target. + */ +int geo_lookup(modsec_rec *msr, geo_rec *georec, const char *target, char **error_msg) +{ + apr_sockaddr_t *addr; + long ipnum = 0; + char *targetip = NULL; + geo_db *geo = msr->txcfg->geo; + char errstr[1024]; + unsigned char buf[2* GEO_MAX_RECORD_LEN]; + const int reclen = 3; /* Algorithm needs changed if this changes */ + apr_size_t nbytes; + unsigned int rec_val = 0; + apr_off_t seekto = 0; + int rc; + int country = 0; + int level; + double dtmp; + int itmp; + + *error_msg = NULL; + + /* init */ + georec->country_code = geo_country_code[0]; + georec->country_code3 = geo_country_code3[0]; + georec->country_name = geo_country_name[0]; + georec->country_continent = geo_country_continent[0]; + georec->region = ""; + georec->city = ""; + georec->postal_code = ""; + georec->latitude = 0; + georec->longitude = 0; + georec->dma_code = 0; + georec->area_code = 0; + + msr_log(msr, 9, "GEO: Looking up \"%s\".", target); + + /* NOTE: This only works with ipv4 */ + if ((rc = apr_sockaddr_info_get(&addr, target, APR_INET, 0, 0, msr->mp)) != APR_SUCCESS) { + + *error_msg = apr_psprintf(msr->mp, "Geo lookup of \"%s\" failed: %s", target, apr_strerror(rc, errstr, 1024)); + return 0; + } + if ((rc = apr_sockaddr_ip_get(&targetip, addr)) != APR_SUCCESS) { + *error_msg = apr_psprintf(msr->mp, "Geo lookup of \"%s\" failed: %s", target, apr_strerror(rc, errstr, 1024)); + return 0; + }; + + /* Why is this in host byte order? */ + ipnum = ntohl(addr->sa.sin.sin_addr.s_addr); + + msr_log(msr, 9, "GEO: Using address \"%s\" (0x%08x).", targetip, ipnum); + + for (level = 31; level >= 0; level--) { + + /* Read the record */ + seekto = 2 * reclen * rec_val; + apr_file_seek(geo->db, APR_SET, &seekto); + /* TODO: check rc */ + rc = apr_file_read_full(geo->db, &buf, (2 * reclen), &nbytes); + + + + /* NOTE: This is hard-coded for size 3 records */ + /* Left */ + if ((ipnum & (1 << level)) == 0) { + rec_val = buf[0] + + (buf[1] << 8) + + (buf[2] << 16); + } + /* Right */ + else { + rec_val = buf[3] + + (buf[4] << 8) + + (buf[5] << 16); + } + + /* If we are past the country offset, then we are done */ + if (rec_val >= geo->ctry_offset) { + break; + } + } + + if (geo->dbtype == GEO_COUNTRY_DATABASE) { + country = rec_val; + country -= geo->ctry_offset; + if (country <= 0) { + *error_msg = apr_psprintf(msr->mp, "No geo data for \"%s\".", target); + return 0; + } + msr_log(msr, 9, "GEO: rec=\"%s\"", log_escape_raw(msr->mp, buf, sizeof(buf))); + + /* Country */ + msr_log(msr, 9, "GEO: country=\"%.*s\"", (1*4), log_escape_raw(msr->mp, (unsigned char *)&rec_val, 1)); + georec->country_code = geo_country_code[country]; + georec->country_code3 = geo_country_code3[country]; + georec->country_name = geo_country_name[country]; + georec->country_continent = geo_country_continent[country]; + } + else { + int field_len = 0; + int rec_offset = 0; + int remaining = GEO_CITY_RECORD_LEN; + unsigned char cbuf[GEO_CITY_RECORD_LEN]; + + seekto = rec_val + (2 * reclen - 1) * geo->ctry_offset; + apr_file_seek(geo->db, APR_SET, &seekto); + /* TODO: check rc */ + rc = apr_file_read_full(geo->db, &cbuf, sizeof(cbuf), &nbytes); + + country = cbuf[0]; + if (country <= 0) { + *error_msg = apr_psprintf(msr->mp, "No geo data for \"%s\".", target); + return 0; + } + msr_log(msr, 9, "GEO: rec=\"%s\"", log_escape_raw(msr->mp, cbuf, sizeof(cbuf))); + + /* Country */ + msr_log(msr, 9, "GEO: country=\"%.*s\"", (1*4), log_escape_raw(msr->mp, cbuf, sizeof(cbuf))); + georec->country_code = geo_country_code[country]; + georec->country_code3 = geo_country_code3[country]; + georec->country_name = geo_country_name[country]; + georec->country_continent = geo_country_continent[country]; + rec_offset++; + remaining -= rec_offset; + + /* Region */ + field_len = strnlen((const char *)cbuf+rec_offset,remaining); + msr_log(msr, 9, "GEO: region=\"%.*s\"", ((field_len+1)*4), log_escape_raw(msr->mp, cbuf, sizeof(cbuf))+(rec_offset*4)); + georec->region = apr_pstrmemdup(msr->mp, (const char *)cbuf+rec_offset, (remaining)); + rec_offset += field_len + 1; + remaining -= field_len + 1; + + /* City */ + field_len = strnlen((const char *)cbuf+rec_offset,remaining); + msr_log(msr, 9, "GEO: city=\"%.*s\"", ((field_len+1)*4), log_escape_raw(msr->mp, cbuf, sizeof(cbuf))+(rec_offset*4)); + georec->city = apr_pstrmemdup(msr->mp, (const char *)cbuf+rec_offset, (remaining)); + rec_offset += field_len + 1; + remaining -= field_len + 1; + + /* Postal Code */ + field_len = strnlen((const char *)cbuf+rec_offset,remaining); + msr_log(msr, 9, "GEO: postal_code=\"%.*s\"", ((field_len+1)*4), log_escape_raw(msr->mp, cbuf, sizeof(cbuf))+(rec_offset*4)); + georec->postal_code = apr_pstrmemdup(msr->mp, (const char *)cbuf+rec_offset, (remaining)); + rec_offset += field_len + 1; + remaining -= field_len + 1; + + /* Latitude */ + msr_log(msr, 9, "GEO: latitude=\"%.*s\"", (3*4), log_escape_raw(msr->mp, cbuf, sizeof(cbuf))+(rec_offset*4)); + dtmp = cbuf[rec_offset] + + (cbuf[rec_offset+1] << 8) + + (cbuf[rec_offset+2] << 16); + georec->latitude = dtmp/10000 - 180; + rec_offset += 3; + remaining -= 3; + + + /* Longitude */ + msr_log(msr, 9, "GEO: longitude=\"%.*s\"", (3*4), log_escape_raw(msr->mp, cbuf, sizeof(cbuf))+(rec_offset*4)); + dtmp = cbuf[rec_offset] + + (cbuf[rec_offset+1] << 8) + + (cbuf[rec_offset+2] << 16); + georec->longitude = dtmp/10000 - 180; + rec_offset += 3; + remaining -= 3; + + /* dma/area codes are in city rev1 and US only */ + msr_log(msr, 9, "GEO: dma/area=\"%.*s\"", (3*4), log_escape_raw(msr->mp, cbuf, sizeof(cbuf))+(rec_offset*4)); + if (geo->dbtype == GEO_CITY_DATABASE_1 + && georec->country_code[0] == 'U' + && georec->country_code[1] == 'S') + { + /* DMA Code */ + itmp = cbuf[rec_offset] + + (cbuf[rec_offset+1] << 8) + + (cbuf[rec_offset+2] << 16); + georec->dma_code = itmp / 1000; + georec->area_code = itmp % 1000; + rec_offset += 6; + remaining -= 6; + } + + } + + *error_msg = apr_psprintf(msr->mp, "Geo lookup of \"%s\" succeeded.", target); + return 1; +} + +/** + * Frees the resources used for Geo lookups + */ +apr_status_t geo_cleanup(modsec_rec *msr) +{ + return APR_SUCCESS; +} + + diff --git a/apache2/msc_geo.h b/apache2/msc_geo.h new file mode 100644 index 00000000..418cd0b3 --- /dev/null +++ b/apache2/msc_geo.h @@ -0,0 +1,62 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * 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 _MSC_GEO_H_ +#define _MSC_GEO_H_ + +#define GEO_STRUCT_INFO_MAX_SIZE 20 +#define GEO_DB_INFO_MAX_SIZE 100 +#define GEO_COUNTRY_OFFSET 0xffff00 +#define GEO_MAX_RECORD_LEN 4 +#define GEO_COUNTRY_UNKNOWN "Unknown" +#define GEO_CITY_UNKNOWN "Unknown" +#define GEO_CITY_RECORD_LEN 50 +#define GEO_COUNTRY_DATABASE 1 +#define GEO_CITY_DATABASE_0 6 +#define GEO_CITY_DATABASE_1 2 + +typedef struct geo_rec geo_rec; +typedef struct geo_db geo_db; + +#include +#include "modsecurity.h" + +/* Structures */ + +struct geo_rec { + const char *country_code; + const char *country_code3; + const char *country_name; + const char *country_continent; + const char *region; + const char *city; + const char *postal_code; + float latitude; + float longitude; + int dma_code; + int area_code; +}; + +struct geo_db { + apr_file_t *db; + const char *dbfn; + int dbtype; + unsigned int ctry_offset; +}; + +/* Functions */ + +int DSOLOCAL geo_init(directory_config *dcfg, const char *dbfn, char **error_msg); + +int DSOLOCAL geo_lookup(modsec_rec *msr, geo_rec *rec, const char *target, char **error_msg); + +apr_status_t DSOLOCAL geo_cleanup(modsec_rec *msr); + +#endif diff --git a/apache2/msc_util.c b/apache2/msc_util.c index 3284b390..f9ebb611 100644 --- a/apache2/msc_util.c +++ b/apache2/msc_util.c @@ -447,6 +447,18 @@ char *log_escape_header_name(apr_pool_t *mp, const char *text) { return _log_escape(mp, (const unsigned char *)text, text ? strlen(text) : 0, 0, 1); } +char *log_escape_raw(apr_pool_t *mp, const unsigned char *text, unsigned long int text_length) { + unsigned char *ret = apr_palloc(mp, text_length * 4 + 1); + unsigned long int i, j; + + for (i = 0, j = 0; i < text_length; i++, j += 4) { + apr_snprintf((char *)ret+j, 5, "\\x%02x", text[i]); + } + ret[text_length * 4] = '\0'; + + return (char *)ret; +} + /** * Transform input into a form safe for logging. */ diff --git a/apache2/msc_util.h b/apache2/msc_util.h index 9a969c70..f1ee8169 100644 --- a/apache2/msc_util.h +++ b/apache2/msc_util.h @@ -61,6 +61,8 @@ char DSOLOCAL *log_escape_nq_ex(apr_pool_t *p, const char *text, unsigned long i char DSOLOCAL *log_escape_header_name(apr_pool_t *p, const char *text); +char *log_escape_raw(apr_pool_t *mp, const unsigned char *text, unsigned long int text_length); + char DSOLOCAL *_log_escape(apr_pool_t *p, const unsigned char *input, unsigned long int input_length, int escape_quotes, int escape_colon); diff --git a/apache2/re.h b/apache2/re.h index 442e418e..203a5089 100644 --- a/apache2/re.h +++ b/apache2/re.h @@ -36,7 +36,6 @@ typedef struct msre_action msre_action; #include "persist_dbm.h" #include "apache2.h" - /* Actions, variables, functions and operator functions */ int DSOLOCAL expand_macros(modsec_rec *msr, msc_string *var, msre_rule *rule, apr_pool_t *mptmp); diff --git a/apache2/re_operators.c b/apache2/re_operators.c index fccfb217..50ab6518 100644 --- a/apache2/re_operators.c +++ b/apache2/re_operators.c @@ -12,6 +12,7 @@ */ #include "re.h" #include "msc_pcre.h" +#include "msc_geo.h" #include "apr_strmatch.h" /** @@ -560,6 +561,102 @@ static int msre_op_validateSchema_execute(modsec_rec *msr, msre_rule *rule, msre #endif +/** + * Perform geograpical lookups on an IP/Host. + */ +static int msre_op_geoLookup_execute(modsec_rec *msr, msre_rule *rule, msre_var *var, + char **error_msg) +{ + geo_rec rec; + geo_db *geo = msr->txcfg->geo; + const char *geo_host = var->value; + msc_string *s = NULL; + int rc; + + if (geo == NULL) { + msr_log(msr, 1, "Geo lookup for \"%s\" attempted without a database. Set SecGeoLookupDb.", geo_host); + return 0; + } + + + rc = geo_lookup(msr, &rec, geo_host, error_msg); + if (rc <= 0) { + return rc; + } + + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "GEO: %s={country_code=%s, country_code3=%s, country_name=%s, country_continent=%s, region=%s, city=%s, postal_code=%s, latitude=%f, longitude=%f, dma_code=%d, area_code=%d}", + geo_host, + rec.country_code, + rec.country_code3, + rec.country_name, + rec.country_continent, + rec.region, + rec.city, + rec.postal_code, + rec.latitude, + rec.longitude, + rec.dma_code, + rec.area_code); + } + + s = (msc_string *)apr_pcalloc(msr->mp, sizeof(msc_string)); + s->name = apr_pstrdup(msr->mp, "country_code"); + s->value = apr_pstrdup(msr->mp, rec.country_code ? rec.country_code : ""); + s->value_len = strlen(s->value); + apr_table_setn(msr->geo_vars, s->name, (void *)s); + + s = (msc_string *)apr_pcalloc(msr->mp, sizeof(msc_string)); + s->name = apr_pstrdup(msr->mp, "country_code3"); + s->value = apr_pstrdup(msr->mp, rec.country_code3 ? rec.country_code3 : ""); + s->value_len = strlen(s->value); + apr_table_setn(msr->geo_vars, s->name, (void *)s); + + s = (msc_string *)apr_pcalloc(msr->mp, sizeof(msc_string)); + s->name = apr_pstrdup(msr->mp, "region"); + s->value = apr_pstrdup(msr->mp, rec.region ? rec.region : ""); + s->value_len = strlen(s->value); + apr_table_setn(msr->geo_vars, s->name, (void *)s); + + s = (msc_string *)apr_pcalloc(msr->mp, sizeof(msc_string)); + s->name = apr_pstrdup(msr->mp, "city"); + s->value = apr_pstrdup(msr->mp, rec.city ? rec.city : ""); + s->value_len = strlen(s->value); + apr_table_setn(msr->geo_vars, s->name, (void *)s); + + s = (msc_string *)apr_pcalloc(msr->mp, sizeof(msc_string)); + s->name = apr_pstrdup(msr->mp, "postal_code"); + s->value = apr_pstrdup(msr->mp, rec.postal_code ? rec.postal_code : ""); + s->value_len = strlen(s->value); + apr_table_setn(msr->geo_vars, s->name, (void *)s); + + s = (msc_string *)apr_pcalloc(msr->mp, sizeof(msc_string)); + s->name = apr_pstrdup(msr->mp, "latitude"); + s->value = apr_psprintf(msr->mp, "%f", rec.latitude); + s->value_len = strlen(s->value); + apr_table_setn(msr->geo_vars, s->name, (void *)s); + + s = (msc_string *)apr_pcalloc(msr->mp, sizeof(msc_string)); + s->name = apr_pstrdup(msr->mp, "longitude"); + s->value = apr_psprintf(msr->mp, "%f", rec.longitude); + s->value_len = strlen(s->value); + apr_table_setn(msr->geo_vars, s->name, (void *)s); + + s = (msc_string *)apr_pcalloc(msr->mp, sizeof(msc_string)); + s->name = apr_pstrdup(msr->mp, "dma_code"); + s->value = apr_psprintf(msr->mp, "%d", rec.dma_code); + s->value_len = strlen(s->value); + apr_table_setn(msr->geo_vars, s->name, (void *)s); + + s = (msc_string *)apr_pcalloc(msr->mp, sizeof(msc_string)); + s->name = apr_pstrdup(msr->mp, "area_code"); + s->value = apr_psprintf(msr->mp, "%d", rec.area_code); + s->value_len = strlen(s->value); + apr_table_setn(msr->geo_vars, s->name, (void *)s); + + return 1; +} + /* rbl */ static int msre_op_rbl_execute(modsec_rec *msr, msre_rule *rule, msre_var *var, char **error_msg) { @@ -1153,6 +1250,13 @@ void msre_engine_register_default_operators(msre_engine *engine) { #endif + /* geoLookup */ + msre_engine_op_register(engine, + "geoLookup", + NULL, + msre_op_geoLookup_execute + ); + /* rbl */ msre_engine_op_register(engine, "rbl", diff --git a/apache2/re_variables.c b/apache2/re_variables.c index ff54a850..faefac89 100644 --- a/apache2/re_variables.c +++ b/apache2/re_variables.c @@ -548,6 +548,50 @@ static int var_tx_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, return count; } +/* GEO */ + +static int var_geo_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const apr_array_header_t *arr = NULL; + const apr_table_entry_t *te = NULL; + int i, count = 0; + + arr = apr_table_elts(msr->geo_vars); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + msc_string *str = (msc_string *)te[i].val; + int match; + + /* Figure out if we want to include this variable. */ + match = 0; + if (var->param == NULL) match = 1; /* Unconditional inclusion. */ + else { + if (var->param_data != NULL) { /* Regex. */ + char *my_error_msg = NULL; + if (!(msc_regexec((msc_regex_t *)var->param_data, str->name, + str->name_len, &my_error_msg) == PCRE_ERROR_NOMATCH)) match = 1; + } else { /* Simple comparison. */ + if (strcasecmp(str->name, var->param) == 0) match = 1; + } + } + + /* If we had a match add this argument to the collection. */ + if (match) { + msre_var *rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + + rvar->value = str->value; + rvar->value_len = str->value_len; + rvar->name = apr_psprintf(mptmp, "GEO:%s", log_escape_nq(mptmp, str->name)); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + count++; + } + } + + return count; +} + /* IP */ static int var_ip_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, @@ -1865,6 +1909,17 @@ void msre_engine_register_default_variables(msre_engine *engine) { PHASE_REQUEST_HEADERS ); + /* GEO */ + msre_engine_variable_register(engine, + "GEO", + VAR_LIST, + 1, 1, + var_generic_list_validate, + var_geo_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + /* IP */ msre_engine_variable_register(engine, "IP", diff --git a/doc/modsecurity2-apache-reference.xml b/doc/modsecurity2-apache-reference.xml index 2564e55d..02b51d94 100644 --- a/doc/modsecurity2-apache-reference.xml +++ b/doc/modsecurity2-apache-reference.xml @@ -1018,6 +1018,28 @@ SecAuditLogStorageDir logs/audit phase. +
+ <literal>SecGeoLookupsDb</literal> + + Description: Defines the path to + the geograpical database file. + + Syntax: SecGeoLookupsDb /path/to/db + + Example Usage: SecGeoLookupsDb + /usr/local/geo/data/GeoLiteCity.dat + + Processing Phase: N/A + + Scope: + Any + + Dependencies/Notes: Check out + www.maxmind.com for free database files. +
+
<literal>SecGuardianLog</literal> @@ -2059,6 +2081,80 @@ SecRule ENV:tag "suspicious" SecRule FILES_TMPNAMES "@inspectFile /path/to/inspect_script.pl"
+
+ <literal moreinfo="none">GEO</literal> + + GEO is a collection populated by the @geoLookups operator. It can be used to match + geographical fields looked up by an IP address or hostname. + + Available since 2.2.0. + + Fields: + + + + COUNTRY_CODE: Two character + country code. EX: US, UK, etc. + + + + COUNTRY_CODE3: Up to three + character country code. + + + + COUNTRY_NAME: The full + country name. + + + + COUNTRY_CONTINENT: The teo + character continent that the country is located. EX: EU + + + + REGION: The two character + region. For US, this is state. For Canada, providence, etc. + + + + CITY: The city name. + + + + POSTAL_CODE: The postal + code. + + + + LATITUDE: The + latitude. + + + + LONGITUDE: The + longitude. + + + + DMA_CODE: The metropoliton + area code. (US only) + + + + AREA_CODE: The phone system + area code. (US only) + + + + Example: + + SecRule REMOTE_ADDR "@geoLookup" chain,drop,msg:'Non-UK IP address' +SecRule GEO:COUNTRY_CODE "!@streq UK" +
+
<literal moreinfo="none">PATH_INFO</literal> @@ -4124,6 +4220,22 @@ SecRule ARGS:route "!@endsWith %{REQUEST_ADDR}" role="bold">@ge 15"
+
+ <literal>geoLookup</literal> + + Description: This operator looks + up various data fields from an IP address or hostname. The results will + be captured in the GEO + collection. + + You must provide a database via SecGeoLookupsDb before this operator can be + used. + + See the GEO variable for an + example and more information on various fields available. +
+
<literal>gt</literal> @@ -4383,4 +4495,4 @@ SecRule XML "@validateSchema /path/to/apache2/conf/xml.xsd
- \ No newline at end of file +