# HG changeset patch # User Dan # Date 1195429495 18000 # Node ID 5bcdee999015a73ff3efb5ff102ef5fdb7dd6e1a # Parent 06db76725891c0b50c9ea0d9c8744680493d02fb Major fixes to the ban system - large IP match lists don't slow down the server miserably anymore. diff -r 06db76725891 -r 5bcdee999015 includes/common.php --- a/includes/common.php Sat Nov 17 23:30:23 2007 -0500 +++ b/includes/common.php Sun Nov 18 18:44:55 2007 -0500 @@ -250,6 +250,12 @@ @call_user_func('page_'.$p[1].'_'.$p[0].'_preloader'); } + // One quick security check... + if ( !is_valid_ip($_SERVER['REMOTE_ADDR']) ) + { + die('SECURITY: spoofed IP address'); + } + $session->start(); $paths->init(); diff -r 06db76725891 -r 5bcdee999015 includes/functions.php --- a/includes/functions.php Sat Nov 17 23:30:23 2007 -0500 +++ b/includes/functions.php Sun Nov 18 18:44:55 2007 -0500 @@ -2727,7 +2727,7 @@ { $array[$i] = decode_unicode_url($val); } - else + else if ( is_array($val) ) { $array[$i] = decode_unicode_array($val); } @@ -2991,6 +2991,72 @@ return $ips; } +/** + * Parses a valid IP address range into a regular expression. + * @param string IP range string + * @return string + */ + +function parse_ip_range_regex($range) +{ + // Regular expression to test the range string for validity + $regex = '/^(([0-9]+(-[0-9]+)?)(\|([0-9]+(-[0-9]+)?))*)\.' + . '(([0-9]+(-[0-9]+)?)(\|([0-9]+(-[0-9]+)?))*)\.' + . '(([0-9]+(-[0-9]+)?)(\|([0-9]+(-[0-9]+)?))*)\.' + . '(([0-9]+(-[0-9]+)?)(\|([0-9]+(-[0-9]+)?))*)$/'; + if ( !preg_match($regex, $range) ) + { + return false; + } + $octets = array(0 => array(), 1 => array(), 2 => array(), 3 => array()); + list($octets[0], $octets[1], $octets[2], $octets[3]) = explode('.', $range); + $return = '^'; + foreach ( $octets as $octet ) + { + // alternatives array + $alts = array(); + if ( strpos($octet, '|') ) + { + $particles = explode('|', $octet); + } + else + { + $particles = array($octet); + } + foreach ( $particles as $atom ) + { + // each $atom will be either + if ( strval(intval($atom)) == $atom ) + { + $alts[] = $atom; + continue; + } + else + { + // it's a range - parse it out + $alt2 = int_range($atom); + if ( !$alt2 ) + return false; + foreach ( $alt2 as $neutrino ) + $alts[] = $neutrino; + } + } + $alts = array_unique($alts); + $alts = '|' . implode('|', $alts) . '|'; + // we can further optimize/compress this by weaseling our way into using some character ranges + for ( $i = 1; $i <= 25; $i++ ) + { + $alts = str_replace("|{$i}0|{$i}1|{$i}2|{$i}3|{$i}4|{$i}5|{$i}6|{$i}7|{$i}8|{$i}9|", "|{$i}[0-9]|", $alts); + } + $alts = str_replace("|1|2|3|4|5|6|7|8|9|", "|[1-9]|", $alts); + $alts = '(' . substr($alts, 1, -1) . ')'; + $return .= $alts . '\.'; + } + $return = substr($return, 0, -2); + $return .= '$'; + return $return; +} + function password_score_len($password) { if ( !is_string($password) ) diff -r 06db76725891 -r 5bcdee999015 includes/pageutils.php --- a/includes/pageutils.php Sat Nov 17 23:30:23 2007 -0500 +++ b/includes/pageutils.php Sun Nov 18 18:44:55 2007 -0500 @@ -23,6 +23,7 @@ function checkusername($name) { global $db, $session, $paths, $template, $plugins; // Common objects + $name = str_replace('_', ' ', $name); $q = $db->sql_query('SELECT username FROM ' . table_prefix.'users WHERE username=\'' . $db->escape(rawurldecode($name)) . '\''); if ( !$q ) { diff -r 06db76725891 -r 5bcdee999015 includes/sessions.php --- a/includes/sessions.php Sat Nov 17 23:30:23 2007 -0500 +++ b/includes/sessions.php Sun Nov 18 18:44:55 2007 -0500 @@ -151,7 +151,7 @@ */ //var $valid_username = '([A-Za-z0-9 \!\@\(\)-]+)'; - var $valid_username = '([^<>_&\?\'"%\n\r\t\a\/]+)'; + var $valid_username = '([^<>&\?\'"%\n\r\t\a\/]+)'; /** * What we're allowed to do as far as permissions go. This changes based on the value of the "auth" URI param. @@ -559,7 +559,7 @@ * @return string 'success' on success, or error string on failure */ - function login_with_crypto($username, $aes_data, $aes_key, $challenge, $level = USER_LEVEL_MEMBER) + function login_with_crypto($username, $aes_data, $aes_key_id, $challenge, $level = USER_LEVEL_MEMBER) { global $db, $session, $paths, $template, $plugins; // Common objects @@ -570,9 +570,9 @@ // Fetch our decryption key - $aes_key = $this->fetch_public_key($aes_key); + $aes_key = $this->fetch_public_key($aes_key_id); if(!$aes_key) - return 'Couldn\'t look up public key "'.$aes_key.'" for decryption'; + return 'Couldn\'t look up public key "'.htmlspecialchars($aes_key_id).'" for decryption'; // Convert the key to a binary string $bin_key = hexdecode($aes_key); @@ -587,6 +587,7 @@ $success = false; // Escaped username + $username = str_replace('_', ' ', $username); $db_username_lower = $this->prepare_text(strtolower($username)); $db_username = $this->prepare_text($username); @@ -702,6 +703,10 @@ $pass_hashed = ( $already_md5ed ) ? $password : md5($password); + // Replace underscores with spaces in username + // (Added in 1.0.2) + $username = str_replace('_', ' ', $username); + // Perhaps we're upgrading Enano? if($this->compat) { @@ -837,7 +842,7 @@ /** * Registers a session key in the database. This function *ASSUMES* that the username and password have already been validated! - * Basically the session key is a base64-encoded cookie (encrypted with the site's private key) that says "u=[username];p=[sha1 of password]" + * Basically the session key is a hex-encoded cookie (encrypted with the site's private key) that says "u=[username];p=[sha1 of password];s=[unique key id]" * @param int $user_id * @param string $username * @param string $password @@ -896,7 +901,7 @@ } /** - * Identical to register_session in nature, but uses the old login/table structure. DO NOT use this. + * Identical to register_session in nature, but uses the old login/table structure. DO NOT use this except in the upgrade script under very controlled circumstances. * @see sessionManager::register_session() * @access private */ @@ -1338,59 +1343,79 @@ function check_banlist() { global $db, $session, $paths, $template, $plugins; // Common objects - if($this->compat) - $q = $this->sql('SELECT ban_id,ban_type,ban_value,is_regex FROM '.table_prefix.'banlist ORDER BY ban_type;'); - else - $q = $this->sql('SELECT ban_id,ban_type,ban_value,is_regex,reason FROM '.table_prefix.'banlist ORDER BY ban_type;'); - if(!$q) $db->_die('The banlist data could not be selected.'); - $banned = false; - while($row = $db->fetchrow()) + $col_reason = ( $this->compat ) ? '"No reason entered (session manager is in compatibility mode)" AS reason' : 'reason'; + $is_banned = false; + if ( $this->user_logged_in ) { - if($this->compat) - $row['reason'] = 'None available - session manager is in compatibility mode'; - switch($row['ban_type']) + // check by IP, email, and username + $sql = "SELECT $col_reason, ban_value, ban_type, is_regex FROM " . table_prefix . "banlist WHERE \n" + . " ( ban_type = " . BAN_IP . " AND is_regex = 0 ) OR \n" + . " ( ban_type = " . BAN_IP . " AND is_regex = 1 AND '{$_SERVER['REMOTE_ADDR']}' REGEXP ban_value ) OR \n" + . " ( ban_type = " . BAN_USER . " AND is_regex = 0 AND ban_value = '{$this->username}' ) OR \n" + . " ( ban_type = " . BAN_USER . " AND is_regex = 1 AND '{$this->username}' REGEXP ban_value ) OR \n" + . " ( ban_type = " . BAN_EMAIL . " AND is_regex = 0 AND ban_value = '{$this->email}' ) OR \n" + . " ( ban_type = " . BAN_EMAIL . " AND is_regex = 1 AND '{$this->email}' REGEXP ban_value ) \n" + . " ORDER BY ban_type ASC;"; + $q = $this->sql($sql); + if ( $db->numrows() > 0 ) { - case BAN_IP: - if(intval($row['is_regex'])==1) { - if(preg_match('#'.$row['ban_value'].'#i', $_SERVER['REMOTE_ADDR'])) + while ( list($reason, $ban_value, $ban_type, $is_regex) = $db->fetchrow_num() ) + { + if ( $ban_type == BAN_IP && $row['is_regex'] != 1 ) { + // check range + $regexp = parse_ip_range_regex($ban_value); + if ( !$regexp ) + { + continue; + } + if ( preg_match("/$regexp/", $_SERVER['REMOTE_ADDR']) ) + { + $banned = true; + } + } + else + { + // User is banned $banned = true; - $reason = $row['reason']; } } - else { - if($row['ban_value']==$_SERVER['REMOTE_ADDR']) { $banned = true; $reason = $row['reason']; } - } - break; - case BAN_USER: - if(intval($row['is_regex'])==1) { - if(preg_match('#'.$row['ban_value'].'#i', $this->username)) + } + $db->free_result(); + } + else + { + // check by IP only + $sql = "SELECT $col_reason, ban_value, ban_type, is_regex FROM " . table_prefix . "banlist WHERE + ( ban_type = " . BAN_IP . " AND is_regex = 0 ) OR + ( ban_type = " . BAN_IP . " AND is_regex = 1 AND '{$_SERVER['REMOTE_ADDR']}' REGEXP ban_value ) + ORDER BY ban_type ASC;"; + $q = $this->sql($sql); + if ( $db->numrows() > 0 ) + { + while ( list($reason, $ban_value, $ban_type, $is_regex) = $db->fetchrow_num() ) + { + if ( $ban_type == BAN_IP && $row['is_regex'] != 1 ) { + // check range + $regexp = parse_ip_range_regex($ban_value); + if ( !$regexp ) + continue; + if ( preg_match("/$regexp/", $_SERVER['REMOTE_ADDR']) ) + { + $banned = true; + } + } + else + { + // User is banned $banned = true; - $reason = $row['reason']; } } - else { - if($row['ban_value']==$this->username) { $banned = true; $reason = $row['reason']; } - } - break; - case BAN_EMAIL: - if(intval($row['is_regex'])==1) { - if(preg_match('#'.$row['ban_value'].'#i', $this->email)) - { - $banned = true; - $reason = $row['reason']; - } - } - else { - if($row['ban_value']==$this->email) { $banned = true; $reason = $row['reason']; } - } - break; - default: - die('Ban error: rule "'.$row['ban_value'].'" has an invalid type ('.$row['ban_type'].')'); } + $db->free_result(); } - if($banned && $paths->get_pageid_from_url() != $paths->nslist['Special'].'CSS') + if ( $banned && $paths->get_pageid_from_url() != $paths->nslist['Special'].'CSS' ) { // This guy is banned - kill the session, kill the database connection, bail out, and be pretty about it die_semicritical('Ban notice', '