8.8 HTTP 8.9 FTP PHP FTP Functions The FTP functions give client access to file servers through the File Transfer Protocol (FTP). The FTP functions are used to open, login and close connections, as well as upload, download, rename, delete, and get information on files from file servers. Not all of the FTP functions will work with every server or return the same results. The FTP functions became available with PHP 3. If you only wish to read from or write to a file on an FTP server, consider using the ftp:// wrapper with the Filesystem functions which provide a simpler and more intuitive interface. Installation For these functions to work, you have to compile PHP with --enable-ftp. The Windows version of PHP has built-in support for this extension. PHP FTP Functions Function Description ftp_alloc() Allocates space for a file to be uploaded to the FTP server ftp_cdup() Changes to the parent directory on the FTP server ftp_chdir() Changes the current directory on the FTP server ftp_chmod() Sets permissions on a file via FTP ftp_close() Closes an FTP connection ftp_connect() Opens an FTP connection ftp_delete() Deletes a file on the FTP server ftp_exec() Executes a command on the FTP server ftp_fget() Downloads a file from the FTP server and saves it into an open local file ftp_fput() Uploads from an open file and saves it to a file on the FTP server ftp_get() Downloads a file from the FTP server ftp_get_option() Returns runtime options of the FTP connection ftp_login() Logs in to the FTP connection ftp_mdtm() Returns the last modified time of a specified file ftp_mkdir() Creates a new directory on the FTP server 148
ftp_mlsd() Returns the list of files in the specified directory ftp_nb_continue() Continues retrieving/sending a file (non-blocking) ftp_nb_fget() Downloads a file from the FTP server and saves it into an open file (non-blocking) ftp_nb_fput() Uploads from an open file and saves it to a file on the FTP server (non-blocking) ftp_nb_get() Downloads a file from the FTP server (non-blocking) ftp_nb_put() Uploads a file to the FTP server (non-blocking) ftp_nlist() Returns a list of files in the specified directory on the FTP server ftp_pasv() Turns passive mode on or off ftp_put() Uploads a file to the FTP server ftp_pwd() Returns the current directory name ftp_quit() ftp_raw() Alias of ftp_close() ftp_rawlist() Sends a raw command to the FTP server ftp_rename() Returns a list of files with file information from a specified directory ftp_rmdir() Renames a file or directory on the FTP server ftp_set_option() Deletes an empty directory on the FTP server ftp_site() Sets runtime options for the FTP connection ftp_size() Sends an FTP SITE command to the FTP server ftp_ssl_connect() Returns the size of the specified file ftp_systype() Opens a secure SSL-FTP connection Returns the system type identifier of the FTP server PHP Predefined FTP Constants Constant Type Description FTP_ASCII Integer FTP_AUTOSEEK Integer FTP_AUTORESUME Integer FTP_BINARY Integer FTP_FAILED Integer Asynchronous transfer has failed FTP_FINISHED Integer Asynchronous transfer is completed FTP_IMAGE Integer Alias of FTP_BINARY FTP_MOREDATA Integer Asynchronous transfer is in progress FTP_TEXT Integer Alias of FTP_ASCII FTP_TIMEOUT_SEC Integer The timeout used for network operations FTP_USEPASVADDRESS Boolean 149
8.10 MySQLI PHP MySQLi Functions The MySQLi functions allows you to access MySQL database servers. Note: The MySQLi extension is designed to work with MySQL version 4.1.13 or newer. Installation / Runtime Configuration For the MySQLi functions to be available, you must compile PHP with support for the MySQLi extension. The MySQLi extension was introduced with PHP version 5.0.0. The MySQL Native Driver was included in PHP version 5.3.0. For installation details, go to: http://php.net/manual/en/mysqli.installation.php For runtime configuration details, go to: http://php.net/manual/en/mysqli.configuration.php PHP MySQLi Functions Description Returns the number of affected rows in the previous MySQL operation Function Turns on or off auto-committing database modifications mysqli_affected_rows() Starts a transaction mysqli_autocommit() Changes the user of the specified database connection mysqli_begin_transaction() Returns the default character set for the database connection mysqli_change_user() Closes a previously opened database connection mysqli_character_set_name() Commits the current transaction mysqli_close() Returns the error code from the last connection error mysqli_commit() Returns the error description from the last connection error mysqli_connect_errno() Opens a new connection to the MySQL server mysqli_connect_error() Adjusts the result pointer to an arbitrary row in the result-set mysqli_connect() Performs debugging operations mysqli_data_seek() Dumps debugging info into the log mysqli_debug() Returns the last error code for the most recent function call mysqli_dump_debug_info() Returns a list of errors for the most recent function call mysqli_errno() Returns the last error description for the most recent function call mysqli_error_list() Fetches all result rows as an associative array, a numeric array, or both mysqli_error() Fetches a result row as an associative, a numeric array, or both mysqli_fetch_all() mysqli_fetch_array() 150
mysqli_fetch_assoc() Fetches a result row as an associative array mysqli_fetch_field_direct() Returns meta-data for a single field in the result set, as an object mysqli_fetch_field() Returns the next field in the result set, as an object mysqli_fetch_fields() Returns an array of objects that represent the fields in a result set mysqli_fetch_lengths() Returns the lengths of the columns of the current row in the result set mysqli_fetch_object() Returns the current row of a result set, as an object Fetches one row from a result-set and returns it as an enumerated mysqli_fetch_row() array Returns the number of columns for the most recent query mysqli_field_count() Sets the field cursor to the given field offset mysqli_field_seek() Returns a character set object mysqli_get_charset() Returns the MySQL client library version mysqli_get_client_info() Returns statistics about client per-process mysqli_get_client_stats() Returns the MySQL client library version as an integer mysqli_get_client_version() Returns statistics about the client connection mysqli_get_connection_stats() Returns the MySQL server hostname and the connection type mysqli_get_host_info() Returns the MySQL protocol version mysqli_get_proto_info() Returns the MySQL server version mysqli_get_server_info() Returns the MySQL server version as an integer mysqli_get_server_version() Returns information about the most recently executed query mysqli_info() Initializes MySQLi and returns a resource for use with mysqli_real_connect() mysqli_init() Returns the auto-generated id used in the last query Asks the server to kill a MySQL thread mysqli_insert_id() Checks if there are more results from a multi query mysqli_kill() Performs one or more queries on the database mysqli_more_results() Prepares the next result set from mysqli_multi_query() mysqli_multi_query() Sets extra connect options and affect behavior for a connection mysqli_next_result() Pings a server connection, or tries to reconnect if the connection has mysqli_options() gone down mysqli_ping() Prepares an SQL statement for execution mysqli_poll() Performs a query against the database mysqli_prepare() Opens a new connection to the MySQL server mysqli_query() Escapes special characters in a string for use in an SQL statement mysqli_real_connect() Executes an SQL query mysqli_real_escape_string() Returns the result from async query mysqli_real_query() Refreshes tables or caches, or resets the replication server information mysqli_reap_async_query() Rolls back the current transaction for the database mysqli_refresh() Changes the default database for the connection mysqli_rollback() Sets the default client character set mysqli_select_db() mysqli_set_charset() Unsets user defined handler for load local infile command mysqli_set_local_infile_default( ) Set callback function for LOAD DATA LOCAL INFILE command mysqli_set_local_infile_handler () 151
mysqli_sqlstate() Returns the SQLSTATE error code for the last MySQL operation mysqli_ssl_set() mysqli_stat() Used to establish secure connections using SSL mysqli_stmt_init() Returns the current system status mysqli_store_result() Initializes a statement and returns an object for use with mysqli_thread_id() mysqli_stmt_prepare() mysqli_thread_safe() Transfers a result set from the last query mysqli_use_result() mysqli_warning_count() Returns the thread ID for the current connection Returns whether the client library is compiled as thread-safe Initiates the retrieval of a result set from the last query executed using the mysqli_real_query() Returns the number of warnings from the last query in the connection 8.11 String PHP String Functions PHP String Functions The PHP string functions are part of the PHP core. No installation is required to use these functions. Function Description addcslashes() Returns a string with backslashes in front of the specified characters addslashes() Returns a string with backslashes in front of predefined characters bin2hex() Converts a string of ASCII characters to hexadecimal values chop() Removes whitespace or other characters from the right end of a string chr() Returns a character from a specified ASCII value chunk_split() Splits a string into a series of smaller parts convert_cyr_string() Converts a string from one Cyrillic character-set to another convert_uudecode() Decodes a uuencoded string convert_uuencode() Encodes a string using the uuencode algorithm count_chars() Returns information about characters used in a string crc32() Calculates a 32-bit CRC for a string crypt() One-way string hashing echo() Outputs one or more strings explode() Breaks a string into an array 152
fprintf() Writes a formatted string to a specified output stream get_html_translation_table() Returns the translation table used by htmlspecialchars() and htmlentities() hebrev() Converts Hebrew text to visual text hebrevc() Converts Hebrew text to visual text and new lines (\\n) into <br> hex2bin() Converts a string of hexadecimal values to ASCII characters html_entity_decode() Converts HTML entities to characters htmlentities() Converts characters to HTML entities htmlspecialchars() Converts some predefined characters to HTML entities htmlspecialchars_decode() Converts some predefined HTML entities to characters implode() Returns a string from the elements of an array join() Alias of implode() lcfirst() Converts the first character of a string to lowercase levenshtein() Returns the Levenshtein distance between two strings localeconv() Returns locale numeric and monetary formatting information ltrim() Removes whitespace or other characters from the left side of a string md5() Calculates the MD5 hash of a string md5_file() Calculates the MD5 hash of a file metaphone() Calculates the metaphone key of a string money_format() Returns a string formatted as a currency string nl_langinfo() Returns specific local information nl2br() Inserts HTML line breaks in front of each newline in a string number_format() Formats a number with grouped thousands ord() Returns the ASCII value of the first character of a string parse_str() Parses a query string into variables print() Outputs one or more strings printf() Outputs a formatted string quoted_printable_decode() Converts a quoted-printable string to an 8-bit string quoted_printable_encode() Converts an 8-bit string to a quoted printable string quotemeta() Quotes meta characters rtrim() Removes whitespace or other characters from the right side of a string setlocale() Sets locale information sha1() Calculates the SHA-1 hash of a string sha1_file() Calculates the SHA-1 hash of a file similar_text() Calculates the similarity between two strings soundex() Calculates the soundex key of a string 153
sprintf() Writes a formatted string to a variable sscanf() str_getcsv() Parses input from a string according to a format str_ireplace() str_pad() Parses a CSV string into an array str_repeat() str_replace() Replaces some characters in a string (case-insensitive) str_rot13() str_shuffle() Pads a string to a new length str_split() str_word_count() Repeats a string a specified number of times strcasecmp() strchr() Replaces some characters in a string (case-sensitive) strcmp() strcoll() Performs the ROT13 encoding on a string strcspn() strip_tags() Randomly shuffles all characters in a string stripcslashes() stripos() Splits a string into an array stripslashes() stristr() Count the number of words in a string strlen() strnatcasecmp() Compares two strings (case-insensitive) strnatcmp() strncasecmp() Finds the first occurrence of a string inside another string (alias of strstr()) strncmp() strpbrk() Compares two strings (case-sensitive) strpos() strrchr() Compares two strings (locale based string comparison) strrev() Returns the number of characters found in a string before any part of some strripos() specified characters are found strrpos() Strips HTML and PHP tags from a string strspn() strstr() Unquotes a string quoted with addcslashes() Returns the position of the first occurrence of a string inside another string (case- insensitive) Unquotes a string quoted with addslashes() Finds the first occurrence of a string inside another string (case-insensitive) Returns the length of a string Compares two strings using a \"natural order\" algorithm (case-insensitive) Compares two strings using a \"natural order\" algorithm (case-sensitive) String comparison of the first n characters (case-insensitive) String comparison of the first n characters (case-sensitive) Searches a string for any of a set of characters Returns the position of the first occurrence of a string inside another string (case- sensitive) Finds the last occurrence of a string inside another string Reverses a string Finds the position of the last occurrence of a string inside another string (case- insensitive) Finds the position of the last occurrence of a string inside another string (case- sensitive) Returns the number of characters found in a string that contains only characters from a specified charlist Finds the first occurrence of a string inside another string (case-sensitive) 154
strtok() Splits a string into smaller strings strtolower() Converts a string to lowercase letters strtoupper() Converts a string to uppercase letters strtr() Translates certain characters in a string substr() Returns a part of a string substr_compare() Compares two strings from a specified start position (binary safe and optionally substr_count() case-sensitive) substr_replace() Counts the number of times a substring occurs in a string trim() Replaces a part of a string with another string ucfirst() Removes whitespace or other characters from both sides of a string ucwords() Converts the first character of a string to uppercase vfprintf() Converts the first character of each word in a string to uppercase vprintf() Writes a formatted string to a specified output stream vsprintf() Outputs a formatted string wordwrap() Writes a formatted string to a variable Wraps a string to a given number of characters 155
9.0 SECURITY OF WEB SYSTEM 9.1 Input Validation Refer to form validation 9.2 File System Security Filesystem Security 1 Null bytes related issues PHP is subject to the security built into most server systems with respect to permissions on a file and directory basis. This allows you to control which files in the filesystem may be read. Care should be taken with any files which are world readable to ensure that they are safe for reading by all users who have access to that filesystem. Since PHP was designed to allow user level access to the filesystem, it's entirely possible to write a PHP script that will allow you to read system files such as /etc/passwd, modify your ethernet connections, send massive printer jobs out, etc. This has some obvious implications, in that you need to ensure that the files that you read from and write to are the appropriate ones. Consider the following script, where a user indicates that they'd like to delete a file in their home directory. This assumes a situation where a PHP web interface is regularly used for file management, so the Apache user is allowed to delete files in the user home directories. Example #1 Poor variable checking leads to.... <?php // remove a file from the user's home directory $username = $_POST['user_submitted_name']; $userfile = $_POST['user_submitted_filename']; $homedir = \"/home/$username\"; unlink(\"$homedir/$userfile\"); echo \"The file has been deleted!\"; ?> Since the username and the filename are postable from a user form, they can submit a username and a filename belonging to someone else, and delete it even if 1 https://www.php.net/manual/en/security.filesystem.php 156
they're not supposed to be allowed to do so. In this case, you'd want to use some other form of authentication. Consider what could happen if the variables submitted were \"../etc/\" and \"passwd\". The code would then effectively read: Example #2 ... A filesystem attack <?php // removes a file from anywhere on the hard drive that // the PHP user has access to. If PHP has root access: $username = $_POST['user_submitted_name']; // \"../etc\" $userfile = $_POST['user_submitted_filename']; // \"passwd\" $homedir = \"/home/$username\"; // \"/home/../etc\" unlink(\"$homedir/$userfile\"); // \"/home/../etc/passwd\" echo \"The file has been deleted!\"; ?> There are two important measures you should take to prevent these issues. o Only allow limited permissions to the PHP web user binary. o Check all variables which are submitted. Here is an improved script: Example #3 More secure file name checking <?php // removes a file from the hard drive that // the PHP user has access to. $username = $_SERVER['REMOTE_USER']; // using an authentication mechanism $userfile = basename($_POST['user_submitted_filename']); $homedir = \"/home/$username\"; $filepath = \"$homedir/$userfile\"; if (file_exists($filepath) && unlink($filepath)) { $logstring = \"Deleted $filepath\\n\"; } else { $logstring = \"Failed to delete $filepath\\n\"; } $fp = fopen(\"/home/logging/filedelete.log\", \"a\"); fwrite($fp, $logstring); fclose($fp); 157
echo htmlentities($logstring, ENT_QUOTES); ?> However, even this is not without its flaws. If your authentication system allowed users to create their own user logins, and a user chose the login \"../etc/\", the system is once again exposed. For this reason, you may prefer to write a more customized check: Example #4 More secure file name checking <?php $username = $_SERVER['REMOTE_USER']; // using an authentication mechanisim $userfile = $_POST['user_submitted_filename']; $homedir = \"/home/$username\"; $filepath = \"$homedir/$userfile\"; if (!ctype_alnum($username) || !preg_match('/^(?:[a-z0-9_- ]|\\.(?!\\.))+$/iD', $userfile)) { die(\"Bad username/filename\"); } //etc... ?> Depending on your operating system, there are a wide variety of files which you should be concerned about, including device entries (/dev/ or COM1), configuration files (/etc/ files and the .ini files), well known file storage areas (/home/, My Documents), etc. For this reason, it's usually easier to create a policy where you forbid everything except for what you explicitly allow. 9.3 Session Data Security Session Management Basics ¶ Session Security ¶ The session module can not guarantee that the information stored in a session is only viewed by the user who created the session. Additional measures are needed to protect the confidentiality of the session, depending on the value associated with it. 158
The importance of the data carried in the session needs to be assessed and further protection may be deployed; this typically comes at a price, such as reduced convenience for the user. For example, to protect users from a simple social engineering tactic, session.use_only_cookies needs to be enabled. In that case, cookies must be enabled unconditionally on the client side, or sessions will not work. There are several ways to leak an existing session ID to third parties. E.g. JavaScript injections, session IDs in URLs, packet sniffing, physical access to the device, etc. A leaked session ID enables the third party to access all resources associated with a specific ID. First, URLs carrying session IDs. If there are links to an external site or resource, the URL including the session ID might be stored in the external site's referrer logs. Second, a more active attacker might listen to network traffic. If it is unencrypted, session IDs will flow in plain text over the network. The solution is to implement SSL/TLS on the server and make it mandatory for users. HSTS should be used for improved security. Note: Even HTTPS can not protect confidential data at all times. For example the CRIME and Beast vulnerabilities may enable an attacker to read the data. Also, note that many networks employ HTTPS MITM proxies for audit purposes. Attackers may also set up such a proxy. Nonadaptive Session Management ¶ PHP's session manager is adaptive by default currently. An adaptive session manager bears additional risks. As of PHP 5.5.2, session.use_strict_mode is available. When it is enabled, and the session save handler supports it, an uninitialized session ID is rejected and a new one is created. This prevents an attack that forces users to use a known session ID. An attacker may paste links or send emails that contains the session ID. E.g. http://example.com/page.php?PHPSESSID=123456789 if session.use_trans_sid is enabled, the victim will start a session using the session ID provided by the attacker. session.use_strict_mode mitigate this risk. Warning User defined save handlers can also support strict session mode by implementing session ID validation. All user defined save handlers should implement session ID validation. The session ID cookie may be set with the domain, path, httponly, secure and, as of PHP 7.3, SameSite attributes. There is precedence defined by browsers. By using the precedence, an attacker can set session ID that could be used permanently. Use of session.use_only_cookies will not solve this 159
issue. session.use_strict_mode mitigates this risk. With session.use_strict_mode=On, the uninitialized session ID will be refused. Note: Even though session.use_strict_mode mitigates the risk of adaptive session management, an attacker can force users to use an initialized session ID which has been created by an attacker. E.g. JavaScript injection. This attack can be mitigated by this manual's recommendations. By following this manual, developers should enable, session.use_strict_mode, use timestamp based session management, and regenerate session IDs using session_regenerate_id() with recommended procedures. If developers follow all of the above, an attacker generated session ID will eventually be deleted. When an access to an obsolete session occurs, developers should save all active session data of the user. As this information will be relevant for an ensuing investigation. The user should be forcefully logged out of all sessions, i.e. require them to reauthenticate. This prevents attackers from abusing stolen sessions. Warning Access to an obsolete session does not necessarily suggest an attack. An unstable network and/or immediate deletion of the active session will result in legitimate users using obsolete sessions. As of PHP 7.1.0, session_create_id() has been added. This function may be operated to access all active sessions of a user efficiently by prefixing the session IDs with the user ID. Enabling session.use_strict_mode is vital with this setup. Otherwise, malicious users can set malicious session ID for other users. Note: Users prior to PHP 7.1.0 SHOULD use CSPRNG, e.g. /dev/urandom, or random_bytes() and hash functions to generate a new session ID. session_create_id() has collision detection and generates a session ID according to the session's INI settings. Use of session_create_id() is preferred. Session ID Regeneration ¶ session.use_strict_mode is a good mitigation, however not sufficient. Developers must equally use session_regenerate_id() for session security. Session ID regeneration reduces the risk of stolen session IDs, thus session_regenerate_id() must be called periodically. E.g. Regenerate the session ID every 15 minutes for security sensitive content. Even in the case that a session ID is stolen, both the legitimate user's and the attacker's session will expire. In other words access by the user or the attacker will generate an obsolete session access error. Session IDs must be regenerated when user privileges are elevated, such as after authenticating. session_regenerate_id() must be called prior to setting the 160
authentication information to $_SESSION. (As of PHP 7.0.0, session_regenerate_id() saves the current session data automatically in order to save timestamp/etc. to the current session.) Ensure only the new session contains the authenticated flag. Developers must not rely on session ID expiration by session.gc_maxlifetime. Attackers may access a victim's session ID periodically to prevent its expiration and keep exploiting it, including an authenticated session. Instead, developers must implement timestamp based session data management. Warning Although the session manager can manage timestamps transparently, this feature is not implemented. Old session data must be kept until GC. Simultaneously, developers must assure themselves obsolete session data is removed. However, developers must not remove active session data immediately. I.e. session_regenerate_id(true); and session_destroy() must never be called together for an active session. This may sound contradictory, but this is a mandatory requirement. session_regenerate_id() does not delete outdated sessions by default. Obsolete authenticated sessions may be present for use. Developers must prevent outdated sessions to be consumed by anyone. They must prohibit access to obsolete session data by themselves using timestamps. Warning The sudden removal of an active session produces undesirable side effects. Sessions can vanish when there are concurrent connections to the web application and/or the network is unstable. Potential malicious access is undetectable with the sudden removal of active sessions. Instead of deleting outdated sessions immediately, developers must set a short- term expiration time (timestamp) in $_SESSION, and prohibit access to the session data by themselves. Developers must not prohibit access to old session data immediately after session_regenerate_id(). It must be prohibited at a later stage. E.g. a few seconds later for stable networks, like a wired network, and a few minutes later for unstable networks such as cell phones or Wi-Fi. If a user accesses an obsolete session (expired session), access to it should be denied. It is also recommended to remove the authenticated status from all of the user's sessions to as it is likely to represent an attack. 161
Proper use of session.use_only_cookies and session_regenerate_id() can cause personal DoS with undeletable cookies set by attackers. In this case, developers may invite users to remove cookies and advise them they may be affected by a security issue. Attackers may set malicious cookies via a vulnerable web application, an exposed/vicious browser plugin, a physically compromised device, etc. Warning Do not misunderstand the DoS risk. use_strict_mode=On is mandatory for general session ID security! All sites are advised to enable use_strict_mode. DoS can only happen when the account is under attack. A JavaScript injection vulnerability in an application represents the most common cause. Session Data Deletion ¶ Obsolete session data must be inaccessible and deleted. The current session module does not handle this well. Obsolete session data should be removed as soon as possible. However, active sessions must not be removed instantly. To satisfy those requirements, developers must implement timestamp based session data management by themselves. Set and manage expiration timestamp in $_SESSION. Prohibit access to outdated session data. When access to obsolete session data is detected, it is advised to remove all authenticated status from the user's sessions and force them to re- authenticate. Access to obsolete session data can represent an attack. To achieve this, developers must keep track of all active sessions of every user. Note: Access to an obsolete session can also happen because of an unstable network and/or concurrent access to the web site. E.g the server tried to set a new session ID via a cookie, but the Set-Cookie packet may not have reached the client due to loss of connection. One connection may issue a new session ID by session_regenerate_id(), but another concurrent connection may not have received the new session ID yet. Therefore, developers must prohibit access to obsolete session at a later stage. I.e. timestamp based session management is mandatory. In summary, session data must not be destroyed with session_regenerate_id() nor session_destroy(), but timestamps must be used to control access to session data. Let session_gc() remove obsolete data from the session data storage. 162
Session and Locking ¶ Session data is locked to avoid race conditions by default. Locking is mandatory to keep session data consistent across requests. However, session-locking can be abused by attackers to perform DoS attacks. To mitigate the risk of a DoS attack by session-locking, minimize locks. Use read only sessions when session data does not need to be updated. Use the 'read_and_close' option with session_start(). session_start(['read_and_close'=>1]); Close the session as soon as possible after updating $_SESSION by using session_commit(). The current session module does not detect any modification of $_SESSION when the session is inactive. It is the developer's responsibility not to modify $_SESSION when the session is inactive. Active Sessions ¶ Developers should keep track of all active sessions for every user. And notify them of how many active sessions, from which IP (and area), how long it has been active, etc. PHP does not keep track of these. Developers are supposed to do so. Various ways to implement this exist. One possible implementation is setting up a database that keeps track of the required data and stores any relevant information. Since session data is GCed, developers must take care of the GCed data to maintain the active session database consistency. One of the simplest implementations is \"User ID prefixed session ID\" and store the required information in $_SESSION. Many databases posses good performance for selecting string prefix. Developers MAY use session_regenerate_id() and session_create_id() for this. Warning Never employ confidential data as a prefix. If the user ID is confidential, consider using hash_hmac(). Warning Enabling session.use_strict_mode is mandatory for this setup. Ensure it is enabled. Otherwise the active session database can be compromised. Timestamp based session management is mandatory to detect access to obsolete sessions. When access to an obsolete session is detected, authentication flags should be removed from all active sessions of the user. This prevents attackers to keep exploiting stolen sessions. 163
Session and Auto-login ¶ Developers must not use long life session IDs for auto-login because it increases the risk of stolen sessions. An auto-login feature should be implemented by the developer. Use a secure one time hash key as an auto-login key using setcookie(). Use a secure hash stronger than SHA-2. E.g. SHA-256 or greater with random data from random_bytes() or /dev/urandom. If the user is unauthenticated, check whether the one-time auto-login key is valid or not. In the case it is valid, authenticate the user and set a new secure one-time hash key. An auto-login key must only be used once, i.e. never reuse an auto-login key, always generate a new one. An auto-login key is a long life authentication key, it should be protected as much as possible. Use path/httponly/secure/SameSite cookie attributes to secure it. I.e. never transmit the auto-login key unless required. Developer must implement the features that disables auto-login and removes unneeded auto-login key cookie. CSRF (Cross-Site Request Forgeries) attacks ¶ Sessions and authentication do not protect against CSRF attacks. Developers must implement CSRF protection by themselves. output_add_rewrite_var() can be used for CSRF protection. Refer to the manual page for more details. Note: PHP prior to 7.2.0 uses the same output buffer and INI setting as trans sid. Therefore, use of output_add_rewrite_var() with PHP prior to version 7.2.0 is not advised. Most web application frameworks support CSRF protection. Refer to the web application framework manual for more details. As of PHP 7.3 the SameSite attribute for the session cookie can be set. This is an additional measure which can mitigate CSRF vulnerabilities. 164
9.4 Preventing Cross Site Scripting Cross-Site Scripting Attacks (XSS) A cross-site scripting attack is one of the top 5 security attacks carried out on a daily basis across the Internet, and your PHP scripts may not be immune. Also known as XSS, the attack is basically a type of code injection attack which is made possible by incorrectly validating user data, which usually gets inserted into the page through a web form or using an altered hyperlink. The code injected can be any malicious client-side code, such as JavaScript, VBScript, HTML, CSS, Flash, and others. The code is used to save harmful data on the server or perform a malicious action within the user’s browser. Unfortunately, cross-site scripting attacks occurs mostly, because developers are failing to deliver secure code. Every PHP programmer has the responsibility to understand how attacks can be carried out against their PHP scripts to exploit possible security vulnerabilities. Reading this article, you’ll find out more about cross-site scripting attacks and how to prevent them in your code. Learning by Example Let’s take the following code snippet. <form action=\"post.php\" method=\"post\"> <input type=\"text\" name=\"comment\" value=\"\"> <input type=\"submit\" name=\"submit\" value=\"Submit\"> </form> Here we have a simple form in which there is a text box for data input and a submit button. Once the form is submitted, it will submit the data to post.php for processing. Let’s say all post.php does is output the data like so: <?php echo $_POST[\"comment\"]; Without any filtering, a hacker could submit the following through the form which will generates a popup in the browser with the message “hacked”. <script>alert(\"hacked\")</script> This example, despite its being malicious in nature, does not seem to do much harm. But think about what could happen in the JavaScript code was written to steal a user’s cookie and extract sensitive information from it? There are far worse XSS attacks than a simple alert() call. 165
Cross-site scripting attacks can be grouped in two major categories, based on how they deliver the malicious payload: non-persistent XSS, and persistent XSS. Allow me to discuss each type in detail. Non-persistent XSS Also known as reflected XSS attack, meaning that the actual malicious code is not stored on the server but rather gets passed through it and presented to the victim, is the more popular XSS strategy of the two delivery methods. The attack is launched from an external source, such as from an e-mail message or a third-party website. Here’s an example of a portion of a simple search result script: <?php // Get search results based on the query echo \"You searched for: \" . $_GET[\"query\"]; // List search results ... The example can be a very unsecure results page where the search query is displayed back to the user. The problem here is that the $_GET[\"query\"] variable isn’t validated or escaped, therefore an attacker could send the following link to the victim: http://example.com/search.php?query=<script>alert(\"hacked\")</script> Without validation, the page would contain: You searched for: <script>alert(\"hacked\")</script> Persistent XSS This type of attack happens when the malicious code has already slipped through the validation process and it is stored in a data store. This could be a comment, log file, notification message, or any other section on the website which required user input at one time. Later, when this particular information is presented on the website, the malicious code gets executed. Let’s use the following example for a rudimentary file-based comment system. Assuming the same form I presented earlier, let’s say the receiving script simply appends the comment to a data file. 166
<?php file_put_contents(\"comments.txt\", $_POST[\"comment\"], FILE_APPEND); Elsewhere the contents of comments.txt is shown to visitors: <?php echo file_get_contents(\"comments.txt\"); When a user submit a comment it gets saved to the data file. Then the entire file (thus the entire series of comments) is displayed to the readership. If malicious code is submitted then it will be saved and displayed as is without any validation or escaping. Preventing Cross-Site Scripting Attacks Fortunately, as easily as an XSS attack can carried out against an unprotected website, protecting against them are just as easy. Prevention must always be in your thoughts, though, even before you write a single line of code. The first rule which needs to be “enforced” in any web environment (be it development, staging, or production) is never trust data coming from the user or from any other third party sources. This can’t be emphasized enough. Every bit of data must be validated on input and escaped on output. This is the golden rule of preventing XSS. In order to implement solid security measures which prevents XSS attacks, we should be mindful of data validation, data sanitization, and output escaping. Data Validation Data validation is the process of ensuring that your application is running with correct data. If your PHP script expects an integer for user input, then any other type of data would be discarded. Every piece of user data must be validated when it is received to ensure it is of the corrected type, and discarded if it doesn’t pass the validation process. If you wanted to validate a phone number, for example, you would discard any strings containing letters, because a phone number should consist of digits only. You should also take the length of the string into consideration. If you wanted to be more permissive, you could allow a limited set of special characters such as plus, parenthesis, and dashes which are often used in formatting phone numbers specific to your intended locale. <?php 167
// validate a US phone number if (preg_match('/^((1-)?d{3}-)d{3}-d{4}$/', $phone)) { echo $phone . \" is valid format.\"; } Data Sanitization Data sanitization focuses on manipulating the data to make sure it is safe by removing any unwanted bits from the data and normalizing it to the correct form. For example, if you are expecting a plain text string as user input, you may want to remove any HTML markup from it. <?php // sanitize HTML from the comment $comment = strip_tags($_POST[\"comment\"]); Sometimes, data validation and sanitization/normalization can go hand in hand. <?php // normalize and validate a US phone number $phone = preg_replace('/[^d]/', \"\", $phone); $len = strlen($phone); if ($len == 7 || $len == 10 || $len == 11) { echo $phone . \" is valid format.\"; } Output Escaping In order to protect the integrity of displayed/output data, you should escape the data when presenting it to the user. This prevents the browser from applying any unintended meaning to any special sequence of characters that may be found. <?php // escape output sent to the browser echo \"You searched for: \" . htmlspecialchars($_GET[\"query\"]); All Together Now! To better understand the three aspects of data processing, let’s take another look at the file-based comment system from earlier and modify it to make sure it’s secure. The potential vulnerabilities in the code stem from the fact that $_POST[\"comment\"] is blindly appended to the comments.txt file which is then displayed directly to the user. To secure it, the $_POST[\"comment\"] value should be validated and sanitized before it is added to the file, and the file’s contents should be escaped when displayed to the user. 168
<?php // validate comment $comment = trim($_POST[\"comment\"]); if (empty($comment)) { exit(\"must provide a comment\"); } // sanitize comment $comment = strip_tags($comment); // comment is now safe for storage file_put_contents(\"comments.txt\", $comment, FILE_APPEND); // escape comments before display $comments = file_get_contents(\"comments.txt\"); echo htmlspecialchars($comments); The script first validates the incoming comment to make sure a non-zero length string as been provided by the user. After all, a blank comment isn’t very interesting. Data validation needs to happen within a well defined context, meaning that if I expect an integer back from the user, then I validate it accordingly by converting the data into an integer and handle it as an integer. If this results in invalid data, then simply discard it and let the user know about it. Then the script sanitizes the comment by removing any HTML tags it may contain. And finally, the comments are retrieved, filtered, and displayed. Generally the htmlspecialchars() function is sufficient for filtering output intended for viewing in a browser. If you’re using a character encoding in your web pages other than ISO-8859-1 or UTF-8, though, then you’ll want to use htmlentities(). For more information on the two functions, read their respective write-ups in the official PHP documentation. Bear in mind that no single solution exists that is 100% secure on a constantly evolving medium like the Web. Test your validation code thoroughly with the most up to date XSS test vectors. Using the test data from the following sources should reveal if your code is still prone to XSS attacks. RSnake XSS cheatsheet (a pretty comprehensive list of XSS vectors you can use to test your code) Zend Framework’s XSS test data XSS cheatsheet (makes use of HTML5 features) 169
Summary Hopefully this article gave you a good explanation of what cross-site scripting attacks are and how you can prevent them from happening to your code. Never trust data coming from the user or from any other third party sources. You can protect yourself by validating the incoming values in a well defined context, sanitizing the data to protect your code, and escaping output to protect your users. After you’ve written your code, be sure your efforts work correctly by testing the code as thoroughly as you can. Image via Inge Schepers / Shutterstock And if you enjoyed reading this post, you’ll love Learnable; the place to learn fresh skills and techniques from the masters. Members get instant access to all of SitePoint’s ebooks and interactive online courses, like Jump Start PHP. 9.5 SQL Injection2 SQL Injection ¶ Many web developers are unaware of how SQL queries can be tampered with, and assume that an SQL query is a trusted command. It means that SQL queries are able to circumvent access controls, thereby bypassing standard authentication and authorization checks, and sometimes SQL queries even may allow access to host operating system level commands. Direct SQL Command Injection is a technique where an attacker creates or alters existing SQL commands to expose hidden data, or to override valuable ones, or even to execute dangerous system level commands on the database host. This is accomplished by the application taking user input and combining it with static parameters to build an SQL query. The following examples are based on true stories, unfortunately. Owing to the lack of input validation and connecting to the database on behalf of a superuser or the one who can create users, the attacker may create a superuser in your database. Example #1 Splitting the result set into pages ... and making superusers (PostgreSQL) 2 https://www.php.net/manual/en/security.database.sql-injection.php 170
<?php $offset = $argv[0]; // beware, no input validation! $query = \"SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offse t;\"; $result = pg_query($conn, $query); ?> Normal users click on the 'next', 'prev' links where the $offset is encoded into the URL. The script expects that the incoming $offset is a decimal number. However, what if someone tries to break in by appending a urlencode()'d form of the following to the URL 0; insert into pg_shadow(usename,usesysid,usesuper,usecatupd,passwd) select 'crack', usesysid, 't','t','crack' from pg_shadow where usename='postgres'; -- If it happened, then the script would present a superuser access to him. Note that 0; is to supply a valid offset to the original query and to terminate it. Note: It is common technique to force the SQL parser to ignore the rest of the query written by the developer with -- which is the comment sign in SQL. A feasible way to gain passwords is to circumvent your search result pages. The only thing the attacker needs to do is to see if there are any submitted variables used in SQL statements which are not handled properly. These filters can be set commonly in a preceding form to customize WHERE, ORDER BY, LIMIT and OFFSET clauses in SELECT statements. If your database supports the UNION construct, the attacker may try to append an entire query to the original one to list passwords from an arbitrary table. Using encrypted password fields is strongly encouraged. Example #2 Listing out articles ... and some passwords (any database server) <?php $query = \"SELECT id, name, inserted, size FROM products WHERE size = '$size'\"; $result = odbc_exec($conn, $query); ?> 171
The static part of the query can be combined with another SELECT statement which reveals all passwords: ' union select '1', concat(uname||'-'||passwd) as name, '1971-01-01', '0' from usertable; -- If this query (playing with the ' and --) were assigned to one of the variables used in $query, the query beast awakened. SQL UPDATE's are also susceptible to attack. These queries are also threatened by chopping and appending an entirely new query to it. But the attacker might fiddle with the SET clause. In this case some schema information must be possessed to manipulate the query successfully. This can be acquired by examining the form variable names, or just simply brute forcing. There are not so many naming conventions for fields storing passwords or usernames. Example #3 From resetting a password ... to gaining more privileges (any database server) <?php $query = \"UPDATE usertable SET pwd='$pwd' WHERE uid='$uid';\"; ?> But if a malicious user submits the value ' or uid like'%admin% to $uid to change the admin's password, or simply sets $pwd to hehehe', trusted=100, admin='yes to gain more privileges, then, the query will be twisted: <?php // $uid: ' or uid like '%admin% $query = \"UPDATE usertable SET pwd='...' WHERE uid='' or uid like '%admin%';\"; // $pwd: hehehe', trusted=100, admin='yes $query = \"UPDATE usertable SET pwd='hehehe', trusted=100, admin='yes' WHERE ...;\"; ?> A frightening example of how operating system level commands can be accessed on some database hosts. Example #4 Attacking the database hosts operating system (MSSQL Server) 172
<?php $query = \"SELECT * FROM products WHERE id LIKE '%$prod%'\"; $result = mssql_query($query); ?> If attacker submits the value a%' exec master..xp_cmdshell 'net user test testpass /ADD' -- to $prod, then the $query will be: <?php $query = \"SELECT * FROM products WHERE id LIKE '%a%' exec master..xp_cmdshell 'net user test testpass /ADD' --%'\"; $result = mssql_query($query); ?> MSSQL Server executes the SQL statements in the batch including a command to add a new user to the local accounts database. If this application were running as sa and the MSSQLSERVER service is running with sufficient privileges, the attacker would now have an account with which to access this machine. Note: Some of the examples above is tied to a specific database server. This does not mean that a similar attack is impossible against other products. Your database server may be similarly vulnerable in another manner. Image courtesy of » xkcd Avoidance Techniques While it remains obvious that an attacker must possess at least some knowledge of the database architecture in order to conduct a successful attack, obtaining this information is often very simple. For example, if the database is part of an open source or other publicly-available software package with a default installation, this information is completely open and available. This information may also be divulged by closed-source code - even if it's encoded, obfuscated, or compiled - 173
and even by your very own code through the display of error messages. Other methods include the user of common table and column names. For example, a login form that uses a 'users' table with column names 'id', 'username', and 'password'. These attacks are mainly based on exploiting the code not being written with security in mind. Never trust any kind of input, especially that which comes from the client side, even though it comes from a select box, a hidden input field or a cookie. The first example shows that such a blameless query can cause disasters. o Never connect to the database as a superuser or as the database owner. Use always customized users with very limited privileges. o Use prepared statements with bound variables. They are provided by PDO, by MySQLi and by other libraries. o Check if the given input has the expected data type. PHP has a wide range of input validating functions, from the simplest ones found in Variable Functions and in Character Type Functions (e.g. is_numeric(), ctype_digit() respectively) and onwards to the Perl compatible Regular Expressions support. o If the application waits for numerical input, consider verifying data with ctype_digit(), or silently change its type using settype(), or use its numeric representation by sprintf(). Example #5 A more secure way to compose a query for paging <?php settype($offset, 'integer'); $query = \"SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;\"; // please note %d in the format string, using %s would be meaningless $query = sprintf(\"SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET %d;\", $offset); ?> o If the database layer doesn't support binding variables then quote each non numeric user supplied value that is passed to the database with the database-specific string escape function (e.g. mysql_real_escape_string(), sqlite_escape_string(), etc.). Generic functions like addslashes() are useful only in a very specific environment 174
(e.g. MySQL in a single-byte character set with disabled NO_BACKSLASH_ESCAPES) so it is better to avoid them. o Do not print out any database specific information, especially about the schema, by fair means or foul. See also Error Reporting and Error Handling and Logging Functions. o You may use stored procedures and previously defined cursors to abstract data access so that users do not directly access tables or views, but this solution has another impacts. Besides these, you benefit from logging queries either within your script or by the database itself, if it supports logging. Obviously, the logging is unable to prevent any harmful attempt, but it can be helpful to trace back which application has been circumvented. The log is not useful by itself, but through the information it contains. More detail is generally better than less. 175
REFERENCES https://www.geeksforgeeks.org/server-side-client-side-programming/ https://microchipdeveloper.com/tcpip:client-server-programming-model https://en.wikipedia.org/wiki/ https://www.php.net https://riptutorial.com/php/example/787/input-and-output-handling 176
Search
Read the Text Version
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179