-
OWASP A6 – Security Misconfiguration with PHP
This will be another non-development related post. I am going to cover security configuration of the operating system, web server, and PHP environment for your web applications. It doesn’t matter how secure your application is if the OS, web server, or PHP configuration is insecure. I am not going to cover full hardening of your servers, but rather some general guidelines along with some specific configuration settings for PHP and Apache.
General guidance on server deployment for your application environment:
- Apply all security related patches to the operating system and services in use. Make sure that Apache and PHP are fully patched.
- Apply a security best practice standard, deviating only when necessary. CIS is a good choice here because of their depth of configuration standards. Use security best practice standards for the OS as well as Apache.
- Change default passwords for all accounts. Use a long and strong password for service and administrative accounts.
- Disable or remove all unnecessary protocols, accounts, scripts, processes, and services.
- Perform vulnerability scans, web application scans, and network and application level penetration tests against your systems on a regular basis.
- Configure servers to log all security related events and to forward those events to a centralized security information management system.
- Configure applications to only display generic error messages.
- Perform administrative actions using unprivileged accounts. Use the “Run As” feature of Windows or the sudo feature of Linux to perform privileged operations on servers.
The above suggestions will help ensure that your system is patched and the OS is configured securely. Apache must be configured securely as well to limit the servers exposure to risk. General Apache recommendations:
- Compile Apache with the minimum amount of modules and features required to run your application(s). I suggest the following directives be run as part of the configuration at a minimum: –enable-headers, –enable-expires, –enable-ssl, –enable-rewrite, –disable-status, –disable-asis, –disable-autoindex, –disable-userdir. The enable settings ensure that you can configure the server to timeout sessions and send other security related responses, support connections over SSL, and rewrite requests to prevent specific HTTP methods. The options for disabling features prevents information disclosure issues within the Apache web server.
- Remove all default scripts from the /cgi-bin directory.
- Create an apache user and group with minimal permissions. Run apache as this user and change ownership of all files served by Apache to this user and group with minimal permissions (in Linux: chown -R apache.apache /path/to/web/directory, chmod -R 644 /path/to/web/directory, then chmod 744 for all directories under the web directory).
- Consider installing and configuring ModSecurity.
Specific Apache web server configuration suggestions follow. Configure httpd.conf so that the server doesn’t report full version and module information:
ServerTokens Prod ServerSignature Off
Configure the server to use an unprivileged user account and group:User apache Group apache
Load the least amount of modules possible for your environment, our server is set to:LoadModule php5_module /usr/modules/libphp5.so LoadModule security2_module /usr/modules/mod_security2.so LoadModule unique_id_module /usr/modules/mod_unique_id.so
Disable the use of unnecessary and potentially dangerous HTTP/WebDAV methods:<Directory /> <Limit OPTIONS PUT CONNECT PATCH PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK DELETE TRACK> Order deny,allow Deny from all </Limit> Options None AllowOverride None Order deny,allow Deny from all </Directory> <Directory "/var/www/htdocs"> <Limit OPTIONS PUT CONNECT PATCH PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK DELETE TRACK> Order deny,allow Deny from all </Limit> Options None AllowOverride None Order allow,deny Allow from all <Directory>
Disable support for all but specifically allowed file extensions:// Match all files and deny <FilesMatch "^.*\.[a-zA-Z0-9]+$"> Order deny,allow Deny from all </FilesMatch> // Allow specific file extensions <FilesMatch "^.*\.(ico|css|tpl|wsdl|html|htm|JS|js|pdf|doc|xml|gif|jpg|jpe?g|png|php)$"> Order deny,allow Allow from all </FilesMatch>
Log errors:ErrorLog "/var/log/apache/error_log" LogLevel notice LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined LogFormat "%h %l %u %t \"%r\" %>s %b" combined CustomLog "/var/log/apache/access_log" combined
Disable access to the cgi-bin directory:<Directory "/usr/local/apache/cgi-bin"> AllowOverride None Options None Order deny,allow Deny from all </Directory>
Block the TRACE and TRACK HTTP Methods (must be added to each virtual host):RewriteEngine On RewriteCond %{REQUEST_METHOD} ^(TRACE|TRACK) RewriteRule .* - [F]
Block anything other than HTTP 1.1 traffic:RewriteCond %{THE_REQUEST} !HTTP/1\.1$ RewriteRule .* - [F]
Applications that require authentication should require SSL for the entire session. The following configuration directive will redirect requests to http://myapp.mysite.com to HTTPS:RewriteCond %{HTTPS} off RewriteRule (myapp.mysite.com.*) https://%{HTTP_HOST}%{REQUEST_URI}
In your ssl configuration file, require SSLv3 or TLS, and only strong encryption protocols:SSLProtocol -ALL +SSLv3 +TLSv1 SSLCipherSuite HIGH:!ADH
If you cannot use the –disable-userdir and –disable-status options during Apache compilation, then add the following directives to your Apache configuration to prevent unnecessary information disclosure related to these modules:UserDir Disabled ExtendedStatus Off
If you cannot use the –disable-autoindex option during Apache compilation, then add the following directive to each <Directory> setting in your Apache configuration to prevent auto indexing of directories and leakage of directory contents:Options -Indexes
Finally, PHP must be configured securely to ensure the protection of your application and company/customer data. General PHP recommendations:- Compile PHP with the minimum amount of modules and features required to run your application(s). I suggest the following directives be run as part of the configuration at a minimum: –with-openssl and –with-mcrypt. This will ensure that you can leverage encryption routines within your application to protect data and passwords.
- Protect the PHP session directory. Place session data in a temporary directory and then apply the most restrictive permissions possible to the folder. The folder can be owned by the root user and the apache group.
- Place third-party PHP libraries used within your applications in a directory outside of the main web directory (/htdocs)
- If possible, apply the PHP Suhosin patch to PHP to provide additional security to the scripting language core.
Specific PHP configuration suggestions follow. Configure php.ini to prevent Denial of Service (DoS) conditions (adjust these settings based on the needs of your application):
// Maximum time a script can execute max_execution_time = 30 // Maximum time a script can spend parsing request data max_input_time = 7200 // Max memory a script can consume memory_limit = 128M // Limit the amount of data that can be POSTed to the // server. This affects file uploads as well. post_max_size = 4M // Limit the maximum size of a file uploaded to the server. upload_max_filesize = 4M // Limit the number of files that can be uploaded at a // single time. max_file_uploads = 10
Enable logging but disable displaying logs to application users:error_reporting = E_ALL & ~E_DEPRECATED display_errors = Off display_startup_errors = Off log_errors = On log_errors_max_len = 1024 // Do not ignore errors, log them all ignore_repeated_errors = Off ignore_repeated_source = Off
Set specific mime types and content types to help prevent encoding, decoding, and other canonicalization issues that can result in successful XSS attacks:default_mimetype = "text/html" default_charset = "ISO-8859-1"
Place third-party PHP applications in a path outside of the /htdocs directory:include_path = ".:/usr/local/apache/phpincludes"
Implement strong protection of PHP sessions:// Save sessions as files in a specific directory session.save_handler = files session.save_path = "/tmp/phpsessions" // Require the use of cookies to prevent session // ID's from being included in URL's session.use_cookies = 1 session.use_only_cookies = 1 session.use_trans_sid = 0 // Set the "secure" and "httponly" flags on the // cookie. This will prevent the cookie from // being sent over an HTTP connection or being // accessed by JavaScript, helping prevent // session hijacking attacks via XSS. session.cookie_secure = true session.cookie_httponly = true // Set cookie path and domain information to // limit where the cookie can be used, thus // protecting session data. session.cookie_path = /codewatch/ session.cookie_domain = www.codewatch.org // Set the cookie to delete once the browser // is closed. session.cookie_lifetime = 0 // Perform garbage collection on session data // after 15 minutes of inactivity. session.gc_maxlifetime = 900 // Use a secure source for generating random // session ID's (set to a non-zero value // on Windows systems. session.entropy_file = /dev/urandom // Use a strong hashing algorithm to create // the session ID and use as many characters // as possible to reduce the likeliness that // the session ID can be guessed or hijacked. session.hash_function = 'sha512' session.hash_bits_per_character = 6 // Send the nocache directive in HTTP(S) // responses to ensure the page can't be // cached. In addition, set the time-to- // live for the page to a low value. session.cache_limiter = nocache session.cache_expire = 15
Disable the ability for PHP to interpret a URL as a file to help prevent some types of remote file include attacks:allow_url_fopen = Off allow_url_include = Off
Disable registration of globals, long arrays, and the argc/argv variables (more information and the reason behind this suggestion can be found here, here, and here).:register_globals = Off register_long_arrays = Off register_argc_argv = Off
Following these guidelines and configuration settings should go a long way towards ensuring the security of your web and application servers, company and customer data, and the integrity of your systems. -
OWASP A4 – Insecure Direct Object References with PHP
Direct object references occur when an application enables a user to provide an actual database key, file name, URL, etc as input and obtains access to data as a result. Our example on OWASP A10 is an example of providing a direct object reference. In our post, the final solution enables the user to supply a database key (0, 1, 2, 3, etc), which will then return the appropriate URL for the redirection. The OWASP A10 solution is a secure direct object reference because it validates that the user has legitimate access to the resource. However, there is certainly a security benefit to obfuscating references by creating indirect object maps.
Indirect object maps associate a direct reference to a separate value. The user only sees the separate value which prevents an attacker from enumerating valid keys, files, etc. In addition, this can be used to protect access to resources in the event that your application’s access controls are bypassed in some way. The applicable ASVS reference is: “Verify that direct object references are protected, such that only authorized objects are accessible to each user.” We are taking this a step further by creating an indirect object mapping.
The steps for implementing this solution involve:
- Creating a PHP class that initializes the list of keys/files/whatever to be indirectly associated.
- Mapping the keys to a hash value. The reference will be a per-session value based on a combination of the session ID and the key value.
- Providing a method for accessing these mappings and another method for setting additional indirect references.
The code:
class indirObjectMap { private $objectMap; public function __construct($session, $username) { // Omitting DB connection code here. SELECT the ID values // from the URL table. These values will be associated // with an indirect value. Validate that the user has // access by mapping the ACL on the URL to the GroupID // associated with the user. Use a prepared statement, // which will be explained in our series on OWASP A1. $urlValues = $db->prepare("SELECT ID FROM urlmap INNER JOIN (Users) ON urlmap.ACL = Users.GroupID WHERE Users.Username = ?"); $urlValues->bindValue(1, $username, PDO::PARAM_STR); $urlValues->execute(); $urlList = $urlValue->fetchAll(PDO::FETCH_ASSOC); // Loop through the ID values, and associate them with a // hash stored in the objectMap variable. The hash is // generated with a combination of the session ID and the // key value. foreach($urlList as $row) { $this->objectMap[hash("sha256", $session . $row['ID'])] = $row['ID']; } } public function getMap($objectRef) { return $this->objectMap[$objectRef]; } public function setMap($session, $objectValue) { $this->objectMap[hash("sha256", $session . $objectValue)] = $objectValue; } }
The values and mappings could be initialized with a call like this:// Store object in a session variable so that it can be // accessed across multiple pages: $_SESSION['refmap'] = new indirObjectMap(session_id(), $_SESSION['username']);
The values can be retrieved or set with the following (using our dataValidator function created in the XSS posts):
// Get a value using the indirect reference. In this // example, the reference is an alphanumeric hash ('an'), // can be any size, and was passed as a POST parameter: $mapping = $_SESSION['refmap']->getMap(dataValidator('an', $_POST['mapValue'], 0)); // Set a value from a provided key. In this example, the // key is a number ('nu') and shouldn't be greater than // five characters: $_SESSION['refmap']->setMap(session_id(), dataValidator('nu', $key, 5));
Note that the above code WILL NOT work if you change session ID’s on each page load with session_regenerate_id(). If you utilize this function, you could instead perform a per user reference map by passing in the username instead of the session ID. Alternatively, you could store the initial session value in the session state ($_SESSION[‘initial’] = session_id();) and then pass this value to the object instead of the current session ID.And just like that, we have completed over half of the OWASP Top 10 requirements using PHP and Apache. Until next time…
-
OWASP A10 – Unvalidated Redirects and Forwards with PHP
This is going to be a pretty short post. There are no directly associated ASVS requirements for OWASP A10. The closest ASVS requirement is 4.2: “Verify that users can only access URLs for which they possess specific authorization,” which will be covered in this post. The risk here is that an unvalidated redirect that accepts user-supplied data could be leveraged to forward an unsuspecting victim to a malicious site. The victim will trust the initial link because it will appear to be valid. The following controls should be implemented to reduce the risk to your web applications:
- Do not use redirects or forward in your application unless it is absolutely necessary.
- If redirects are necessary, do not allow user-supplied data within the redirection.
- If the user must select an option that impacts the location of the redirection, then a) implement the input validation techniques that were in our XSS prevention series (found here, here, here, and here), b) map the values submitted by users to valid URL’s (expanded upon below), and c) validate the user has permission to access the URL.
Mapping user-supplied values to valid URL’s is the best method for preventing a redirection vulnerability in your web application. Map the user to a security group/role and then map URL’s to the proper groups/roles. This can be performed in the web application configuration or within a database table.
An example table:
ID URL ACL 0 /secure/admin 0 2 http://www.example.com 4 3 subdomain.example.com 1 First, validate the data. In this case, you would have a regular expression in your validation routine that only allows numbers. This could be further improved by supplying a maximum size for the supplied value. If the redirect request parameter passes validation, then supply the data to a function that pulls the URL from a table for the redirect and validates the access.
Using our input validation function, we would call:
// Where urlValue is a number mapped to a URL // but supplied by the user. dataValidator("number", $urlValue, 2);
This would enforce the requirement that the supplied parameter is a number, and is only two digits long. The above would output the data, which would be supplied to a function that supplies the correct URL:// Assign the URL mapped result to a variable // to be included in the redirect. In this // example, $username would be pulled from a // session object: $redirURL = getMappedURL(dataValidator("number", $urlValue, 2), $username); header("Location: " . $redirURL); // Make sure processing stops after redirect die();
The function for pulling the correct URL might look something like (simplistic ACL approach):function getMappedURL($mapValue, $username) { // Omitting DB connection code here. SELECT the URL value // associated to the number parameter supplied by the user // from the database. Validate that the user has access by // mapping the ACL on the URL to the GroupID associated with // the user. Use a prepared statement, which will // be explained in our series on OWASP A1. Return the // associated URL. $urlMap = $db->prepare("SELECT URL FROM urlmap INNER JOIN (Users) ON urlmap.ACL = Users.GroupID WHERE ID = ? AND Users.Username = ?"); $urlMap->bindValue(1, $mapValue, PDO::PARAM_INT); $urlMap->bindValue(2, $username, PDO::PARAM_STR); $urlMap->execute(); $mappedValue = $urlMap->fetch(); // If the query successfully returns the redirect URL // then return the value. Otherwise, return an error // page. if (sizeof($mappedValue) > 1) { return $mappedValue['URL']; } else { return "RedirError.php"; } }
There you have it. Another fairly simple development solution to a potentially dangerous security vulnerability. Enjoy until next time. -
OWASP A9 – Insufficient Transport Layer Protection with PHP
This post will step back from coding a bit to focus on what is usually a web server and scripting language configuration issue. Most, if not all, OWASP A9 issues can be resolved with appropriate configuration of Apache and PHP. Since this is mostly a development related blog, and this topic is not completely development related, I will finish this section all in this post. OWASP A9 is covered by the ASVS 10.x series of controls.
ASVS 10.1 Requirement:
Verify that a path can be built from a trusted CA to each Transport Layer Security (TLS) server certificate, and that each server certificate is valid.
ASVS 10.1 Solution:
SSL/TLS encryption is only as trustworthy and secure as the chain of certificates used to provide the encryption. It is essential that you purchase and install valid certificates from trustworthy CA’s. The certificate in use must be valid, must not have expired, must not have been revoked, must match the domains for which it is being used, must have been signed with a secure algorithm (not MD5), and must have been encrypted with a secure algorithm (RSA 2048 or AES 256 preferably).
ASVS 10.2 Requirement:
Verify that failed TLS connections do not fall back to an insecure connection.
ASVS 10.2 Solution:
This control can be accomplished by disabling all insecure encryption protocols in the Apache web server configuration. Add the following to your SSL settings in the Apache configuration:
SSLProtocol -ALL +TLSv1 +TLSv1.1 +TLSv1.2
This will disable all protocols except for TLS.ASVS 10.3 Requirement:
Verify that TLS is used for all connections (including both external and backend connections) that are authenticated or that involve sensitive data or functions.
ASVS 10.3 Solution:
Use the configuration example provided in the 10.2 solution on all of your web servers. Test all other connections to validate that SSLv3 or TLS is in use. This can be accomplished by attempting to connect with other protocols using the OpenSSL client. The following command can be used to test for SSLv2 for example:
openssl s_client -ssl2 -connect <Server Name or IP>:<port_number>
The above command will result in a short error if the server to which you are connecting does not support SSLv2 (which is a good thing).ASVS 10.4 Requirement:
Verify that backend TLS connection failures are logged.
ASVS 10.4 Solution:
Logging can be added to your VirtualHost configuration for the SSL connection. The configuration option should look something like this:
CustomLog "/var/log/apache/ssl_request_log" \ "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b"
This causes a custom log to be generated at the provided location. The %t will cause the time to be logged, %h logs the IP of the connecting host, %{SSL_PROTOCOL}x will log the protocol version, %{SSL_CIPHER}x will log the cipher used for the connection, %r will log the first line of the request (the GET/POST and page requested), and %b will log the size of the response minus HTTP headers in bytes.ASVS 10.5 Requirement:
Verify that certificate paths are built and verified for all client certificates using configured trust anchors and revocation information.
ASVS 10.5 Solution:
Follow all of the recommendations for our ASVS 10.1 solution when implementing client certificates.
ASVS 10.6 Requirement:
Verify that all connections to external systems that involve sensitive information or functions are authenticated.
ASVS 10.6 Solution:
Review code within your application utilized to connect to external systems and validate that some form of authentication is used if sensitive information is transferred. Authentication can be in the form of certificates or usernames and passwords (for just a few examples).
ASVS 10.7 Requirement:
Verify that all connections to external systems that involve sensitive information or functions use an account that has been set up to have the minimum privileges necessary for the application to function properly.
ASVS 10.7 Solution:
Validate that the account that connects to the external systems has as limited access as possible to those servers or applications. The account should only be able to use the required external functions.
ASVS 10.8 Requirement:
Verify that there is a single standard TLS implementation that is used by the application that is configured to operate in an approved mode of operation (See http://csrc.nist.gov/groups/STM/cmvp/documents/fips140-2/FIPS1402IG.pdf).
ASVS 10.8 Solution:
The easiest way to implement this control is to use a single encryption library (OpenSSL for example) and force the use of TLS through the solution to ASVS 10.2. In addition, disable the use of all weak or medium strength encryption algorithms. This can be accomplished in your Apache SSL configuration with the following line:
SSLCipherSuite HIGH:!ADH:!aNULL:!eNULL:!MD5:!EDH
The above configuration only activates the strong encryption algorithms and then disables ciphers using anonymous Diffie-Hellman key exchange, the MD5 hashing algorithm, or NULL ciphers. I recommend taking this a step further and manually disabling 128bit and Triple-DES encryption algorithms such that the web server will only allow 256bit encryption algorithms. An example would be !SRP-RSA-AES-128-CBC-SHA. To block all low/medium strength ciphers, 128 bit ciphers, RC4 based ciphers, and only allow 256bit PFS ciphers consider something like:SSLCipherSuite HIGH:!ADH:!aNULL:!eNULL:!LOG:!EXP:!PSK:!SRP:!DSS:!MD5: !SEED:!DES:!3DES:!RC4:!AES128:!CAMELLIA:!AES256-GCM-SHA384: !AES256-SHA256:!AES256-SHA
You can check the list of ciphers supported by your system by running:openssl ciphers
ASVS 10.9 Requirement:Verify that specific character encodings are defined for all connections (e.g., UTF-8).
ASVS 10.9 Solution:
The default character set can be configured for PHP by adding the following line to your php.ini file:
default_charset = "UTF-8"
Minor Bonus:There are some additional security controls that can be implemented in your server configuration to help secure web communication. These options include adding the “secure” flag to your cookies, using a strong dhparams value to protect the key exchange, redirecting to HTTPS for sensitive pages, and setting HTTP Strict Transport Security (HSTS).
We have already covered setting the “secure” flag in cookies so I am only going to cover setting strong dhparams values, redirecting to HTTPS, and setting the HSTS header.
In order to configure strong dhparams, you must first create a dhparams file in PEM format. This can be performed with OpenSSL like so:
openssl dhparam -out dhparam4096.pem 4096
Note that this will take a long time to complete. If your Apache installation has been appropriately patched to support DH parameter values, then you can add the following to your configuration:SSLDHParametersFile /path/to/dh4096.pem
Certain sections of your site can be redirected from HTTP to HTTPS in Apache using the following directive:RewriteCond %{HTTPS} off RewriteRule (.*securearea.*) https://%{HTTP_HOST}%{REQUEST_URI}
The above tells Apache to rewrite the URL to use HTTPS when the original connection used HTTP and when the URL contains “securearea” in the URL. Rewrite rules can be written to be much more specific if necessary. More information can be found here.The HSTS header can be added to your site as well. This will tell compliant web agents to replace HTTP links with HTTPS. This can be accomplished via PHP code or in your Apache configuration like so:
// This can be added to your high-level Apache config, // virtual host configurations, specific directories, // or within htaccess files to fine tune where the // header is applied with Apache: Header always set Strict-Transport-Security "max-age=500; includeSubDomains" // PHP code to add to your top/header page: header('Strict-Transport-Security: max-age=500');
You should also enforce SSL on all login pages, pages that require authentication to access, and pages that perform other sensitive operations (such as password resets).OWASP A9 isn’t super exciting stuff, nor does it require cool code (or any code for that matter), but it is important none the less. If you want a good and quick check, Qualys provides a free SSL scanning tool here.
-
OWASP A5 – Cross-Site Request Forgery (CSRF) with PHP
We are going to cover Cross-Site Request Forgery (CSRF) countermeasures in this post. This is an often overlooked but potentially deadly vulnerability that can be easily remediated. CSRF is an attack that enables a malicious website to execute procedures on a web application for which the victim has already authenticated.
For example, suppose a user logs into their bank account and then accidentally browses to a malicious site. The malicious site has code that submits a money transfer request to the banking application. If the user didn’t correctly logout and end the session, and the banking application doesn’t have an anti-CSRF protection in place, then the transaction will be executed. This happens because the browser sends the request along with the session cookie/data, and if the application has no countermeasures in place it has no way of knowing that the transaction was unintended.
There isn’t really a section dedicated to CSRF in the ASVS guide. There is a single control referenced in section 11 due to the fact that while the impact of a CSRF vulnerability can be great, the control is fairly simple to implement.
ASVS 11.7 Requirement:
Verify that the application generates a strong random token as part of all links and forms associated with transactions or accessing sensitive data, and that the application verifies the presence of this token with the proper value for the current user when processing these requests.
ASVS 11.7 Solution:
The first step in protecting your applications against CSRF attacks is to read and implement all of our recommendations for preventing XSS attacks. An XSS vulnerability can be used to bypass CSRF defenses.
The next step in defending against CSRF attacks is to implement a token that is used to identify valid form submissions. This token should be unique to the session, hard to guess (like a session identifier), and sent via the HTTP POST method to prevent logging of the value or storage of the value in browser history (this also makes CSRF attacks slightly more difficult). Modify your login page such that successful authentication results in the creation of a server side session variable in which to store the token:
// Set the token by creating a SHA 512 hash of // the current session identifier. $_SESSION['token'] = hash("sha512", session_id() . openssl_random_pseudo_bytes(32));
This creates a token that is tied to the session and combined with a 32bit length random value, then hashed with a strong hash algorithm. Then, in forms submitting sensitive data (really probably best to just implement this in every form just in case), you would add this as a hidden value:<form method="POST"> <input type="text" name="transaction"> <input type="hidden" name="token" value=" . $_SESSION['token'] . "> </form>
In your form submission validation code, you would validate that the submitted token matches the one created for the session. As always, perform all necessary input validation and output encoding on the value (not show here, please see the XSS posts for more info):// Validate that neither the form token nor the // session token variable are empty and that // both tokens match. if (!empty($_POST['token']) && !empty($_SESSION['token']) && $_SESSION['token'] == $_POST['token']) { // Proceed ... // Set token to a new value $_SESSION['token'] = hash("sha512", session_id() . openssl_random_pseudo_bytes(32)); } else { // Log and present invalid data/submission error }
Not only does the above code validate the token, but it generates and sets a new token for the next successful form entry. This ensures that even if an attacker obtains a CSRF token during the length of a valid session, it can only be used once. It also reduces the risk of useful token theft as the value changes upon each successful modification. And that is really all there is to it. Simple, yet effective defense against CSRF (as long as your site doesn’t contain XSS vulnerabilities). -
OWASP A2 – Cross-Site Scripting (XSS) with PHP Part 4
This is going to be a short post that expands upon input validation controls. These routines are in addition to those found in the OWASP ASVS and should be utilized where possible to help mitigate the risk of any type of injection attack (XSS, SQLi, LDAPi, XMLi, Command injection, etc).
There are two other major forms of validation that can be added into your application security controls to help protect your users, applications, and systems from attack:
- Type Validation: Compare the submitted type of the data against what is expected (integer vs. array, string vs. array, etc)
- Length Validation: Compare the length of the submitted data against that which is expected. For example, if your application contains a form field in which a zip code is used, then the value should be no more than five numbers, a dash, and four more numbers for a total of ten characters.
We will expand upon our whitelist validation routine for ASVS 5.3 found in our first post on XSS countermeasures for these examples.Type Validation:
Type validation can be performed by manually setting the type of the data before passing it to the validation function. For example:
// Manually set the data type to string. We are validating // the value to make sure it possesses all numbers, but the // type is represented as a string of data in PHP. After // setting the type, we can pass it to our validation routine. $myData = (string)$_POST["data"]; $validated = dataValidator("number", $myData); function dataValidator($regex, $data) { $decodeData = html_entity_decode($data, ENT_QUOTES, 'UTF-8'); $typeArray = array( "number" => '/^[0-9]+$/', "letter" => '/^[a-zA-Z]+$/', "alphan" => '/^[a-zA-Z0-9]+$/' ); $sanitizeArray = array( "number" => '/[^0-9]/', "letter" => '/[^a-zA-Z]/', "alphan" => '/[^a-zA-Z0-9]/' ); if (preg_match($typeArray[$regex], $decodeData)) { return $decodeData; } else { $sanitized = preg_replace($sanitizeArray[$regex], '', $decodeData); return $sanitized; } }
PHP provides several types for use in casting. You can probably statically set the type to “string” for most PHP web applications. It is unlikely that you will want to pass arrays in GET/POST requests. In this case, you can change the decode routine to statically set the data type to string:$decodeData = html_entity_decode((string)$data, ENT_QUOTES, 'UTF-8');
Length Validation:We can automatically restrict the size of the input in PHP by using the substr function. This function can be used to just take the desired amount of characters. The function takes in three variables; the string to be used, the start character to use, and the length from the start character to use. Our function can be changed to accept an additional parameter signifying the maximum length of the data:
// Modify the function to accept another variable that // signifies the maximum length of the input data. function dataValidator($regex, $data, $dataLen) { $decodeData = html_entity_decode((string)$data, ENT_QUOTES, 'UTF-8'); $typeArray = array( "number" => '/^[0-9]+$/', "letter" => '/^[a-zA-Z]+$/', "alphan" => '/^[a-zA-Z0-9]+$/' ); $sanitizeArray = array( "number" => '/[^0-9]/', "letter" => '/[^a-zA-Z]/', "alphan" => '/[^a-zA-Z0-9]/' ); // If a value of 0 is passed as the length for // the function, then DON'T truncate the data. // Otherwise, set a variable for how many characters // there should be in the data. if ($dataLen == 0) { // Set the size to the full size of the data. The // strlen function returns an integer representing // the size of the string. $setSize = strlen($decodeData); } else { // Set the size to that which was passed to the function. $setSize = $dataLen; } if (preg_match($typeArray[$regex], $decodeData)) { // Use the PHP substr function to only return the // right number of characters, starting at // character 0. return substr($decodeData, 0, $setSize); } else { $sanitized = preg_replace($sanitizeArray[$regex], '', $decodeData); // Use the PHP substr function to only return the // right number of characters from the sanitized // string, starting at character 0. return substr($sanitized, 0, $setSize); } }
If we passed the following data to the function above, it would return 146342:$myData = "1saa4sdf6sdaf3424"; $validated = dataValidator("number", $myData, 6);Conclusion:
These additional checks will improve the data input into the application while also eliminating many classes of vulnerability. These in no way represent all of the ways in which you can and should perform input validation. Other scenarios could include cases where input data should always start with a specific character, or should always contain a certain series of characters, etc. There are many one off cases unique to applications that can be considered to improve the security and functionality of your web site.
Just like that, we are done with OWASP 2010 A2 – Cross-Site Scripting (XSS). Our next post will cover a new section in the OWASP 2010 Top 10. It will remain a surprise until posted.
-
OWASP A2 – Cross-Site Scripting (XSS) with PHP Part 3
Last week we finished up input validation, which represents one half of the solution towards preventing XSS. This week we will cover the other half; output encoding. Output encoding protects the application in the event that malicious script data somehow makes its way into the database or a form parameter. It is a fail safe if you will.
Let’s begin…
ASVS 6.1 Requirement:
Verify that all untrusted data that are output to HTML (including HTML elements, HTML attributes, javascript data values, CSS blocks, and URI attributes) are properly escaped for the applicable context.
ASVS 6.1 Solution:
Escaping or encoding data prior to output to the browser will help to ensure that malicious code is not executed, and is instead rendered as text. PHP provides two functions for encoding HTML data; htmlentities and htmlspecialchars. We will use htmlentities, as it encodes all characters for which there are HTML character entity equivalents (thorough). The htmlspecialchars function operates as its name suggests; only certain designated special/dangerous characters are encoded (&, ‘, “, <, >).
Example:
$data = htmlentities($_POST['value'], ENT_QUOTES, "UTF-8", false); print("$data");It is just as simple as that. Here we are assigning the POSTed form value of ‘value’ to the $data variable in an encoded form and later printing/outputting it to the browser. The first value passed to htmlentities is the data to be encoded. The following constant tells htmlentities what to do with quotes; ENT_QUOTES provides the most secure mechanism, which is to encode single AND double quote characters. The third variable passed to htmlentities sets the character set to be used. The final variable tells htmlentities whether or not to double encode (encode an already encoded value). Double encoding is unnecessary and could modify how the data is displayed.
This same function should be used to encode user supplied data that is being pulled from a database, xml file, etc. All user supplied data should be output encoded regardless of its origin.
ASVS 6.2 Requirement:
Verify that all output encoding/escaping controls are implemented on the server side.
ASVS 6.2 Solution:
This is an easy one, don’t escape/encode data on the client side as this can be bypassed. Use the htmlentities function within your server side PHP code to output encode user supplied data.
ASVS 6.3 Requirement:
Verify that output encoding /escaping controls encode all characters not known to be safe for the intended interpreter.
ASVS 6.3 Solution:
This was covered above in our solution for 6.1. Use htmlentities instead of htmlspecialchars, and you will be certain to encode everything with an HTML entity equivalent.
Note: We are skipping ASVS 6.4 through 6.8 as they cover escaping for SQL, XML, LDAP and other types of injection that will be reviewed in later blog posts.
ASVS 6.9 Requirement:
Verify that for each type of output encoding/escaping performed by the application, there is a single security control for that type of output for the intended destination.
ASVS 6.9 Solution:
Do not vary your security controls depending upon the destination (SQL database, LDAP, XML, etc). Perform the same type of output encoding for all user supplied data sources and regardless of the destination of the data. This will prevent malicious script code from later being displayed in some way, possibly by a different application.
ASVS 6.10 Requirement:
Verify that all code implementing or using output validation controls is not affected by any malicious code.
ASVS 6.10 Solution:
Scan your code for viruses, or any malicious backdoors. Perform manual and automated code reviews.
And that is it for output encoding. This is a very simple control to implement that will go a long way towards protecting your web applications from XSS attacks.
We have completed the sections in the OWASP ASVS covering controls to prevent XSS attacks. However, the next post will provide an input validation follow up that provides a couple additional security layers to help protect your applications. After that post, we will move on to a new OWASP 2010 Top 10 section.
As always, please provide any feedback you have, positive or negative.
-
OWASP A2 – Cross-Site Scripting (XSS) with PHP Part 2
Today we will finish up ASVS section 5. Next week we will begin ASVS 6.x (Output Encoding/Escaping Requirements). This should be a pretty short post.
ASVS 5.6 Requirement:
Verify that a single input validation control is used by the application for each type of data that is accepted.
ASVS 5.6 Solution:
Watch out for data validation code sprawl. Use the same data validation code for the same data type each time you perform the validation. Don’t rewrite or add the validation code to each page of PHP. Write it once as a function or class and then include that function/class in the pages requiring validation.
ASVS 5.7 Requirement:
Verify that all input validation failures are logged.
ASVS 5.7 Solution:
Record validation failures to a log file, database table, or both. This will help in performing forensics if the site is compromised as well as in responding to incidents/attacks as they occur. I will use the whitelist validation function from last week as an example, removing the comments from that demonstration:
function dataValidator($type, $data) { $decodeData = html_entity_decode($data, ENT_QUOTES, 'UTF-8'); $typeArray = array( "number" => '/^[0-9]+$/', "letter" => '/^[a-zA-Z]+$/', "alphan" => '/^[a-zA-Z0-9]+$/' ); if (preg_match($typeArray[$type], $decodeData)) { return true; } else { // Pseudo code for calling a logging function. // Passing the date/time, type to be tested, // as well as the data that was passed (encoded). writeLog(date('Y-m-d H:m:s') . " - $type Validation failed with the following data: " . htmlentities($decodeData, ENT_QUOTES, UTF-8, false)); return false; } }
ASVS 5.8 Requirement:Verify that all input data is canonicalized for all downstream decoders or interpreters prior to validation.
ASVS 5.8 Solution:
Data that is encoded in a special character set can bypass validation filters or be used in directory traversal attacks in many cases. Canonicalization enforces a specific character set and encodes the data (or modifies the file path) in a way that reduces or eliminates the risk of XSS or directory traversal attacks. This is accomplished through setting a default character set, encoding data, and canonicalizing file paths. Configure the default character set for PHP by adding/modifying this in php.ini:
default_charset = "ISO-8859-1"
User supplied data which contains paths can be canonicalized with the realpath() PHP function:realpath($data);
User supplied data can be encoded into HTML characters using htmlentities or htmlspecialchars. The HTML entities function encodes everything for which there is an HTML character entity equivalent, whereas htmlspecialchars only converts special characters. We will discuss these functions when we cover ASVS 6.x (starting next post).Example code putting it all together:
$safeData = htmlentities(realpath($data), ENT_QUOTES, ISO-8859-1, false);
ASVS 5.9 Requirement:Verify that all input validation controls are not affected by any malicious code.
ASVS 5.9 Solution:
Scan your code for viruses, or any malicious backdoors. Perform manual and automated code reviews.
-
OWASP A2 – Cross-Site Scripting (XSS) with PHP Part 1
We are finally starting a new OWASP Top 10 security risk today. The next few weeks (or possibly month) will cover XSS prevention techniques in PHP. This is probably a little more glamorous/sexy than authentication and session management. Controls to protect an application against XSS are primarily represented in the OWASP ASVS sections 5 and 6. Today, we start with ASVS section 5.
On to the code…
ASVS 5.1 Requirement:
Verify that the runtime environment is not susceptible to buffer overflows, or that security controls prevent buffer overflows.
ASVS 5.1 Solution:
This is more of a patch management issue than a coding problem in most cases (with PHP that is). Make sure your version of PHP is up to date. If using an older version, research potential vulnerabilities, perform a vulnerability scan, and make sure your code is not using any affected modules. However, I suggest using a newer and patched version. Validate that there are no vulnerabilities in your web server software, any frameworks that have been deployed, etc.
ASVS 5.2 Requirement:
Verify that a positive validation pattern is defined and applied to all input.
ASVS 5.2 Solution:
This requires a whitelist validation approach where only known good characters are accepted. Limiting the acceptable input to a specific set of characters reduces the risk of XSS, other forms of injection attacks (SQL injection, LDAP injection, etc), and several other classes of attack. It is also important to decode the input data before performing any validation. Decoding the data and performing canonicalization will help prevent filter bypass attacks that rely on encoding the data in a different character set. This can be implemented fairly easily with a PHP function like so:
// Create a function that takes in the type of data // and the actual data itself. The data type will // be used to determine what kind of regex to use. function dataValidator($type, $data) { // Decode the data into a standard character // set before performing any checks $decodeData = html_entity_decode($data, ENT_QUOTES, 'UTF-8'); // Declare an array that defines each data type // name along with the regex. In this example, // we are only defining three types to accept: // number - only numbers in the data, letter - // only alphabet characters in the data, and // alphan - alphanumeric characters in the data. $typeArray = array( "number" => '/^[0-9]+$/', "letter" => '/^[a-zA-Z]+$/', "alphan" => '/^[a-zA-Z0-9]+$/' ); // Use the PHP preg_match function to determine // if the data only contains those characters. // Select the regex type by passing the "type" // submitted to the function in to the array. // If there is a match, then the data is good // and the if statement returns true, otherwise // return false. if (preg_match($typeArray[$type], $decodeData)) { return true; } else { return false; } }
The above function could be implemented in a file that is included in any page that validates user supplied data. The function could be called with dataValidator(“number”, $_POST[‘userdata’]) (as an example) and the return value evaluated to determine whether bad data was submitted.Whitelist validation is ideal, but sometimes blacklist validation is required or preferable. You can use similar code for a blacklist, just modify the regex and return true if the regex doesn’t match return values:
// In this example, the regex's have been modified to // include everything that SHOULD NOT be found in the // data type. So a number value shouldn't contain // letters from the alphabet or special characters. function dataValidator($type, $data) { $decodeData = html_entity_decode($data, ENT_QUOTES, 'UTF-8'); $typeArray = array( "number" => '/^[a-zA-Z!\*,\'\"\|\.\\/\?\@\$\%\:\+\\(\)\[\]\{\}]+$/', "letter" => '/^[0-9!\*,\'\"\|\.\\/\?\@\$\%\:\+\\(\)\[\]\{\}]+$/', "alphan" => '/^[!\*\'\"\|\\/\?\@\$\%\+\\(\)\[\]\{\}]+$/' ); // Here we return true if the regex DOESN'T match. if (!preg_match($typeArray[$type], $decodeData)) { return true; } else { return false; } }ASVS 5.3 Requirement:
Verify that all input validation failures result in input rejection or input sanitization.
ASVS 5.3 Solution:
Implement a generic error message that is displayed if the return type from dataValidator() is false and reject the input. Alternatively, remove all non-matching characters. You can modify the code above to sanitize user supplied data like so:
function dataValidator($type, $data) { $decodeData = html_entity_decode($data, ENT_QUOTES, 'UTF-8'); $typeArray = array( "number" => '/^[0-9]+$/', "letter" => '/^[a-zA-Z]+$/', "alphan" => '/^[a-zA-Z0-9]+$/' ); // Implement a second array for sanitizing. Anything that // doesn't match will be removed later in the code. $sanitizeArray = array( "number" => '/[^0-9]/', "letter" => '/[^a-zA-Z]/', "alphan" => '/[^a-zA-Z0-9]/' ); if (preg_match($typeArray[$type], $decodeData)) { // Return the input data because it passed the validation. return $decodeData; } else { // The validation failed, therefore some bad data was // passed that needs to be removed. Then return the // sanitized data. $sanitized = preg_replace($sanitizeArray[$type], '', $decodeData); return $sanitized; } }ASVS 5.4 Requirement:
Verify that a character set, such as UTF-8, is specified for all sources of input.
ASVS 5.4 Solution:
This can be accomplished by forcing the data type. If an attacker can force a specific character set, then they might be able to bypass your data validation filters. An example of forcing UTF-8, and silently ignore/drop any characters that can’t be represented by that character set:
// Supply the user supplied data to the forceUTF function. // This function will use the iconv PHP function to convert // the data to UTF-8, and drop any characters that can't // be converted into the charset. function forceUTF($data) { $utfEncoded = iconv("UTF-8", "UTF-8//IGNORE", $data); return $utfEncoded; }ASVS 5.5 Requirement:
Verify that all input validation is performed on the server side.
ASVS 5.5 Solution:
Do not perform input validation on the client side as this is trivial to bypass and gives an attacker clues as to how data might be validated on the server side.
I think this is a good place to leave off for the week. Please provide feedback if you have it.
-
OWASP A3 – Broken Authentication and Session Management Defenses with PHP Part 5
We are finishing up OWASP A3 today. Yay! I haven’t decided which section I will cover next, probably something fun like XSS or SQLi mitigations. Picking up where we left off, we are at ASVS 2.7…
ASVS 2.7 Requirement:
Verify that the strength of any authentication credentials are sufficient to withstand attacks that are typical of the threats in the deployed environment.
ASVS 2.7 Solution:
This can vary depending upon the application and type of data that needs to be protected. In any case, credentials should be hashed rather than encrypted, because a hash cannot be reversed.
A hash can be vulnerable to collision attacks (where two different values result in the same hash) or can be attacked via rainbow tables. Salting the hash with a second strong value can reduce the risk from rainbow tables. The salt should be generated with a sufficient length using a strong algorithm. In addition, the hashing function should have a work factor to increase the time it takes to generate the output, which in turn significantly increases the time it takes to brute force or generate a rainbow table. A great article on how to do this correctly can be found here.
Using a salted hash in PHP can be done like so:
// Create the salt. First generate the size, then create the salt // and encode it $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC); $iv = base64_encode(mcrypt_create_iv($iv_size, MCRYPT_DEV_URANDOM)); // Now we hash the password with Blowfish, salted with the IV created // above, using a work factor of 12: $user_pass = base64_encode(crypt($_POST['user_pass'], "$2y$12$" . $iv));
I am using PHP’s crypt function in this example. It takes the password, combined with the algorithm and work factor ($2y is Blowfish, $12y is the “cost”) and then the salt. This operation is still fairly quick and typically something you only have to perform at login. The data in the $user_name variable would be stored in a field within the database, the salt will need to be stored as well to compare hashes, and the code above would be used to compare what is being submitted vs. the actual password for the user.ASVS 2.8 & 2.9 Requirements:
Verify that all account management functions are at least as resistant to attack as the primary authentication mechanism.
Verify that users can safely change their credentials using a mechanism that is at least as resistant to attack as the primary authentication mechanism.
ASVS 2.8 & 2.9 Solutions:
When implementing password reset or change features, such as through the use of secret questions, hash the answers as described above. In addition, error messages should not indicate whether the questions or answers are incorrect. Instead, provide a generic failure message. Lockout an account if the there are multiple failed attempts to change/reset a password.
ASVS 2.10 Requirement:
Verify that re-authentication is required before any application-specific sensitive operations are permitted.
ASVS 2.10 Solution:
This will help prevent an attack where a user logs into the application and walks away without locking the computer or logging out of the application. This might also help prevent Cross-Site Request Forgery (CSRF) attacks. You can accomplish this with a special session variable that is required to be set and match the user’s password when performing a sensitive operation. Example:
// If the user has supplied the authentication credential, then hash and // compare $_SESSION['reauth'] = base64_encode(crypt($_POST['reauth'], "$2y$12$" . $_SESSION['salt'])); // Check whether the session variable exists, if it hasn't been set, or // if it doesn't match the password, then display the re-authentication // page. If it has been set, then present the page with the sensitive // operation: if (!isset($_SESSION['reauth'] || $_SESSION['reauth'] != $_SESSION['user_pass']) { // authentication code } else { // sensitive operation ... // Reset the authentication credential once the operation has completed $_SESSION['reauth'] = ""; }
ASVS 2.11 Requirement:Verify that after an administratively-configurable period of time, authentication credentials expire.
ASVS 2.11 Solution:
Passwords should not last forever. This requirement can be implemented through the creation of a password date column stored with the account data in the database. Authentication code can check whether the date has exceeded the defined value and force a password change. The date can be loaded into a session variable upon login and then checked:
// Check whether the password is over 90 days old (90x24x60x60), // if so, redirect to the change password script. This would be // part of a header file for each page: if (time() - $_SESSION['Password_Age']) > 7776000) { header('Location: changePassword.php'); die(); }
Update the database value for the password age and the $_SESSION[‘Password_Age’] value once the password has been changed.ASVS 2.12 Requirement:
Verify that all authentication decisions are logged.
ASVS 2.12 Solution:
The application should be designed such that all suspicious activity (malformed data, authentication failures, etc) is logged. Authentication decisions should be logged regardless of success or failure. You can implement PHP code to log to a file, database, SNMP, or many other mechanisms. These log files should be reviewed and correlated with other security information on a regular basis.
ASVS 2.13 Requirement:
Verify that account passwords are salted using a salt that is unique to that account (e.g., internal user ID, account creation) and hashed before storing.
ASVS 2.13 Solution:
See our solution to ASVS 2.7 above. This can be accomplished by using the username as the salt, the unique ID in the database for the account, or some other unique value tied to the account.
ASVS 2.14 Requirement:
Verify that all authentication credentials for accessing services external to the application are encrypted and stored in a protected location (not in source code).
ASVS 2.14 Solution:
Generate or purchase a private key for encrypting/decrypting a credential stored in a separate file. The private key and file should have limited permissions (read only by the application account, in Linux: chmod 400 /path/to/file). Example for decrypting:
// Read the contents of a private key into a variable $pkey = file_get_contents('/path/to/private/key'); // Read the contents of the encrypted value into a variable $cipherText = file_get_contents('/path/to/password/credential'); // Decrypt with OpenSSL using AES 128 bit algorithm $clearText = openssl_decrypt($cipherText, 'aes128', $pkey, false);
ASVS 2.15 Requirement:Verify that all code implementing or using authentication controls is not affected by any malicious code.
ASVS 2.15 Solution:
Scan your code for viruses, or any malicious backdoors. Perform manual and automated code reviews.
Minor Bonus:
In addition to the controls listed above, make sure that you use the HTTP POST method whenever submitting credentials to a web based application. This will prevent the credentials from being stored in the browser history as part of the URL or from being stored in most web server logs (which is what can happen when using GET). Sample form code:
<form method="POST" action="script_file_goes_here.php">
We will finally move on to a new section in our next post. I hope you are excited.