I'm building a private CMS for my own use and am at the point where I will start building out the username and password storing features. I am considering the possibility of storing all admin username, password, and user details in a multidimensional array within a PHP file, rather than using SQL to store them in a database.
My reason for wanting to use this non-traditional approach of storing user info is the belief that this will make it harder for attackers to gain unauthorized access to user info (usernames, passwords, IP addresses, etc.), because I will not be connecting to a MySQL database.
Rough Outline of Code:
add_user.php
// set the last referrer session variable to the current page
$_SESSION['last_referrer'] = 'add_user.php';
// set raw credential variables and salt
$raw_user = $_POST['user'];
$raw_pass = $_POST['pass'];
$raw_IP = $_SERVER['REMOTE_ADDR'];
$salt = '&^${QqiO%Ur!W0,.#.*';
// set the username if its clean, else its false
$username = (is_clean($raw_user)) ? $raw_user : false; // is_clean() is a function I will build to check if strings are clean, and can be appended to an array without creating a parsing error.
// set the salted, sanitized, and encrypted password if its clean, else its false
$password = (is_clean($raw_pass)) ? $salt . encrypt($raw_pass) : false; // encrypt() is a function I will build to encrypt passwords in a specific way
// if username and password are both valid and not false
if( $username && $password ) {
// set the users IP address
$IP = sanitize($raw_IP);
// create a temporary key
$temp_key = $_SESSION['temp_key'] = random_key();
// random_key() is a function I will build to create a key that I will store in a session only long enough to use for adding user info to the database.php file
// add user details array to main array of all users
$add_user = append_array_to_file('database.php', array($username, $password, $IP));
// append_array_to_file() is a function I will build to add array's to the existing multidimensional array that holds all user credentials.
// The function will load the database.php file using cURL so that database.php can check if the temp_key session is set, the append_array_to_file() function will stop and return false if the database.php file reports back that the temp_key is not set.
// The function will crawl database.php to read the current array of users into the function, will then add the current user's credentials to the array, then will rewrite the database.php file with the new array.
// destroy the temporary session key
unset($_SESSION['temp_key']);
}
else {
return false;
}
database.php
$users_credentials = array(1 => array('username' => 'jack',
'password' => '&^${QqiO%Ur!W0,.#.*HuiUn34D09Qi!d}Yt$s',
'ip'=> '127.0.0.1'),
2 => array('username' => 'chris',
'password' => '&^${QqiO%Ur!W0,.#.*8YiPosl#87&^4#',
'ip'=> '873.02.34.7')
);
I would then create custom functions to mimic SQL queries like SELECT for use in verifying users trying to log in.
My Questions
Is this a bad idea, and if so, why?
Am I correct in thinking that this will reduce the number of possibilities for hackers trying to gain unauthorized access, sniff/steal passwords, etc., since I'm not connecting to a remote database?
I don't see any advantage: Whether you use a text file, a mysql database or a php file ( === text file), they are all "databases" in the sense that they are files where you store your information. The difference is that an sql database is made for that stuff;
I do see disadvantages as there are more potential holes you would have to think about. Some examples (apart from the stuff mentioned in the comments):
You need to take care that the password file is always out of the web-root in case php dies on you;
You need to avoid passing around your password file in for example source control.
These are not things that are hard to solve, but using a normal database you don't even have to worry about them.
Apart from that are misunderstanding the purpose of the salt: If you just prepend it to the encrypted password, there is really no point in using a salt, you need to send it to your encrypt function to hash it with your text-password so that rainbow tables would have to be generated for each password instead of just one for your whole database. And for that reason you should also not use a single salt for all your users, each should have a different, unique salt.
If you plan to store any kind of config data in a text file of any sort, as opposed to a traditional database, consider using an .ini file. If I'm not mistaken, you can also take advantage of storing it outside of your web root, just like the php.ini file.
Here's a great post explaining exactly how to go about this: Using ini files for PHP application settings
PHP Manual: get_cfg_var()
Related
My old PHP app has a default admin user and md5 encrypted password created by the SQL that creates the database: insert into users values ( 1, 'admin', MD5('changeMe'), 2 );
Is there a simple way to include a default user and encrypted password using PHP's passowrd_hash function on creating the tables? I ask because I understand that password_hash is a native PHP function and I assume it won't be understood in SQL.
The solution to my problem came in three parts. My OP sought a simple way to create a hashed password for the admin user for insertion in the MySQL database on the installation of the application, using the native PHP password_hash() function.
(1) Based on a suggestion by #Nick and #Tadman, I decided to incorporate setting the hash in an installer script that would set not only the hash but other defined site/application variables.
Rather than inserting user values when the database table is created, it was deferred until immediately after, with the admin user entering their credentials in the form that inserts the hash and writes other definitions to a file:
$userpass = $_POST['userpass'];
echo password_hash($userpass, PASSWORD_BCRYPT);
(2) The second part of my problem was replacing all instances of md5()`` withpassword_hash()` and I achieved that by using a neat PHP script I found online to recursively search and replace the occurrences on the server.
Having replaced the md5() occurrences, I needed to change the hash comparison method and again by searching the relevant files I was able to replace instances of:
if ($p != $theUser->pwd ) {
return( false ); }
with:
if(password_verify($p, $theUser->pwd)) {
// Success!
}
else {
// Invalid credentials
echo "Uh oh!";
}
(3) The third step in resolving the problem was discovering that adding $1$ to the opening of the md5 hash could make it readable by password_hash(); so I just needed to make a couple of adjustments in the installed database to the admin user's old password.
Thanks to those who helped shine the light so I could find my way. I'm off now to invent the wheel and sliced bread.
you can do something like this in php:
$hash = password_hash('changeMe');
//echo $hash;
then use this hash in the Database.
When running two different websites, say free.webhost.com/app1 and free.webhost.com/app2, it seems that Firefox has trouble storing different login credentials for both, especially when the same username is used with different passwords. If a user's credentials on the /app1 site are Name and pass1 and on the other site are Name and pass2, then Firefox can only store one of these and will ask to change the password when hopping between them.
I investigated this problem and to my astonishment this seems to be a WONTFIX in the firefox bug repository: https://bugzilla.mozilla.org/show_bug.cgi?id=263387
Is there any way I can workaround this when designing my apps? Like by setting a certain cookie property in PHP or html, or even specify a (fake) different domain name, so that firefox no longer considers free.webhost.com/app1 and free.webhost.com/app2 as the same website for password storage (and can thus store a different password with the same username for both sites)?
No, there is no workaround or trick for this. Deploy your apps to different domains - even different subdomains (e.g. app1.example.com and app2.example.com) will do.
We need a custom saver. I'm going to try be short and concise.
This is very useful if you don't want the browser saver. I think it can have some applications.
THE BASIC
I suggest in the PHP we use different cookies to save the session with session_name or session.name directive. For each site we must set a session name.
In the HTML we should use different inputs name, so the app1 input will be <input type='email' name='email_app1' /> and app2 email_app2. We also can disable the autocomplete.
The data is saved locally encrypted with AES. For this we can get CryptoJS.
We also want to have salt hash in the client.
THE CONCEPT (summarized):
Save locally the password encrypted. It is returned by the login
controller. Of course if the user wants.
Save a salt which changes in each login. Of course if the user wants.
When the user return to the login page, the JavaScript checks if there
is a salt and it sends it to server. The PHP returns the passphrase and JavaScript decrypts the local password.
the sample code:
In the controller:
// I use functions in the controller like example
public function getLoginPage(){
// it prints the html and js to login using the basics how i have said
}
// salt is sended by the JavaScript
public function getPassphrase( $salt, $username ){
$passPhrase = get_passphrase_from_salt( $salt, $username, Request::IP() );
return $passPhrase;
}
// It is to get the salt
public function getSalt( $username, $password ){
$user = get_user( $username, $password );
// if valid user...
$passphrase = random_string();
$salt = random_string();
$encrypted = encrypt( $password, md5($passphrase) );
save_in_table_salt( $salt, $passphrase, $username, Request::IP() );
// it prints a JSON like example
return json_encode( array( 'salt' => $salt, 'encrypted' => $encrypted) );
}
// ... Normal login etc you could change the salt and reset in the client
In the view we put the JavaScript logic. I used localstorage but I think it's not important.
// in login page
window.onload = function(){
if( localStorage.getItem('salt') !== null ) { // the data is saved
// Get the passphrase
ajax_call('post', 'getPassphrase', {
salt: localStorage.getItem('salt'),
username: localStorage.getItem('username')
}, function( passphrase ){
// It sets the inputs values!
document.getElementById('username_app1').value = localStorage.getItem('username');
document.getElementById('password_app1').value = decrypt( localStorage.getItem('password'), CryptoJS.MD5(passphrase) );
});
}
};
// it captures the submit action
document.getElementById('login_form').onsubmit = function(){
// it asks to user if he wants save locally the credentials
if( localStorage.getItem('salt') === null
&& confirm('Do you want save credentials?') ){
var form = this;
// get salt
ajax_call('post', 'getSalt', {
user: document.getElementById('username_app1').value,
password: document.getElementById('password_app1').value
}, function( object ){
localStorage.setItem('salt', object.salt);
localStorage.setItem('password', object.encrypted);
localStorage.setItem('username', document.getElementById('username_app1').value );
form.submit(); // now yes
});
return false; // it prevents submit
}
};
You must know that the code is a sample. Some functions don't exists and it's only to be understood. We need more conditions and logic to do it works.
UPDATED: Now works with multiple computers and IP security and more!
There is no workaround for this as internal credential storage in Firefox is organized per domain, not per URL.
Even changing Name or ID for input HTML controls or Form tag will not affect this.
Only solution is to host your application on different (sub)domains.
Probably the best solution here would be to create 2 different virtual host for your app.
Like one for webhost.com and one for free.webhost.com.
How To Set Up Apache Virtual Hosts on Ubuntu 12.04 LTS
Hope this help!!
If your are having problem on setting up your virtual host in your server let me know.
Note:You need to create your DNS entry to access the new host you created or you need to add a record to the host file of your system from where you are browsing the site.
I'm trying to create a link that when clicked will login a user automatically and take them to a specific page.
I've thought about creating some sort of hashed string that contains the user's ID, username and a few other pieces of info. When clicked these pieces of information are looked up in the DB and if validated I login them in and redirect them to a specific page.
For sites like Twitter and Facebook when I receive an email notification and click the link in my email I'm automatically taken to my inbox on the corresponding site. I'm trying to duplicate that behavior...
Are there any security issues with doing something like this or is there a safer more preferred way?
if you want to offer this feature to your users, you have to take care of two things:
The validity of the created url must be set in time (ex: 24hours, 48hours).
The created url must only work for one specific user.
(optionnal) The created url only work for one page
I propose this kind of solution to create an url which match these criteria (it's only a proof of concept):
<?php
$privateKey = 'somethingVerySecret';
$userName = 'cedric';
$url = 'my/personal/url';
$timeLimit = new DateTime('Tomorow');
function createToken($privateKey, $url, $userName, $timeLimit){
return hash('sha256', $privateKey.$url.$userName.$timeLimit);
}
function createUrl($privateKey, $url, $userName, $timeLimit){
$hash = createToken($privateKey, $url, $userName, $timeLimit->getTimestamp());
$autoLoginUrl = http_build_query(array(
'name' => $userName,
'timeLimit' => $timeLimit,
'token' => $hash
));
return $url.'?'.$autoLoginUrl;
}
function checkUrl($privateKey){
if((int)$_GET['timeLimit'] > time() ){
return false;
}
//check the user credentials (he exists, he have right on this page)
$hash = createToken($privateKey, $_SERVER['PHP_SELF'], $_GET['name'], $_GET['timeLimit']);
return ($_GET['token'] == $hash);
}
The general standard for logging in is when a user creates an account your program should create a string of seemly random letters and numbers with a certain php function in php 5.5, and then store this in a file with some sort of pointer based on the username. Then when a user tries to login you use that same function and compare the two strings. The function being hash_pbkdf2 even though this php function supports sha... encryptions do not use those. I salt the hash code with the username. Here is an article on all website login and password things. The most secure thing you can do with your website to prevent people from brute force cracking your passwords is to limit the connection speed after a couple wrong password attempt to something so slow it would take longer than the life of the universe to crack after a couple password attempts. If you wanted to make a sort of remember me button store the username in cookies But never the password the browser will take care of the remembering password part if you label your form elements correctly.
I feel a bit anxious to put my database username, password and encryption keys into a PHP file that everyone who hacked himself into the server could easily read. Is it maybe possible to load PHP and tell it the passwords it might need upfront?
My question is simple: Is there any other way to let PHP know about the DB password and key other than to put everything into one file?
Any ideas are welcome.
To my knowledge you will have to declare your variables at some point in order to initiate a DB connection. You can, however, do certain things to make it more difficult for someone to read it.
In addition to safe programming practices mentioned in another answer, you can be sure to only store your passwords in one file (such as Config.php) and include it in your database connection function. Moreover, you can store this variable outside the web root, meaning that people will not be able to access it unless they have server access.
You can also make it more difficult to be read for someone who gains access. This includes encrypting it with a Symmetric Key Algorithm, and then decrypting it when used. Note that the encryption/decryption key will have to also be stored on the server which, again, make this little more than an inconvenience.
Take proper security steps in securing your code, and restrict your database so it can only be accessed locally. This should provide ample security.
Technically speaking, nobody cant see your php scripts. Anyways, I just updated my DB macros to the newest standards. This is how I do things in my CMS:
config.php file:
<?
if (!defined('A52CMS')) {die('What are you looking for? 0.o');}
$config_mysql = array(
'database' => 'databasename1',
'user' => 'username1',
'password' => 'password1',
'table_prefix' => 'a52_'
);
db_macros.php file:
<?
if (!defined('A52CMS')) {die('What are you looking for? 0.o');}
class DB {
protected $default_config = array(
'server' => 'localhost',
'database' => '',
'user' => 'root',
'password' => '',
'table_prefix' => 'a52_',
'mysql_config_array' => 'config_mysql'
);
function __construct ($config_mysql) {
$this->config = array_merge($this->default_config, $config_mysql);
/* There are some secret connection related stuff here */
mysql_query("SET NAMES utf8");
unset($this->config['password']);
unset($GLOBALS[$this->config['mysql_config_array']]);
}
/* There are alot of mysql related functions here */
function __destruct () {
mysql_close($this->conn);
if (#mysql_ping($this->conn)) {
CriticalError('Closing mysql connection was unsuccessful!');
}
}
}
core.php file:
<?
if (!defined('A52CMS')) {die('What are you looking for? 0.o');}
require('config.php');
// Lets try to display the mysql raw data
print_r($config_mysql); // displays something
require('db_macros.php');
$DB = new DB($config_mysql);
// Lets try to display the mysql raw data AGAIN
print_r($config_mysql); // displays nothing
index.php file:
<?
define('A52CMS', true);
require('core.php');
// This where the content starts.. if some plugin is being included, then mysql data is already unset, so basically you cannot hack them..
You might get some ideas how to make your applications little bit more secure. Also you might get some new ideas on how to contruct your custom mysql class (if you havent done so already.)
make secure scripts. If your scripts are secure, there's no way to get hacked
make your scripts which contains password(s) only apache-readable
make sure access to your password files are out of web scope
I did not even see another method to "save" password(s) than these
Ok, i'm wondering if this code stored in user_login.php could be improved or if i'm doing it wrong. I'm confused because all my script in the application are long about 30-40 lines only and i was wondering if i am missing something.
This script is called with an ajax call like everyone else in my application except for template files.
<?php
# Ignore
if (!defined('APP_ON')) { die('Silence...'); }
# Gets the variables sent
$user_name = post('user_name');
$user_password = extra_crypt(post('user_password'));
# Check if the user exists
if (!user::check($user_name, $user_password)) { template::bad($lang['incorrect_login']); }
# Logging in
$id = user::get_id($user_name, $user_password);
$u = new user($id);
$u->login();
template::good($lang['correct_login']);
?>
I'm going to explain it:
# Ignore
if (!defined('APP_ON')) { die('Silence...'); }
This basically check that the file is not called directly. Each url is redirected to an index.php file that manage the url (es: www.mysite.com/user/view/id/1) and include the right file. In the first lines of this index.php file there is a define('APP_ON', true);. The index file also initialize the application calling some set of functions and some classes.
# Gets the variables sent
$user_name = post('user_name');
$user_password = extra_crypt(post('user_password'));
The function post() manage the recovering of $_POST['user_name'] making some checks.
The function extra_crypt() just crypt the password using sha1 and a custom alghoritm.
I'm using prepared statement for sql queries so don't worry about escaping post variables.
# Check if the user exists
if (!user::check($user_name, $user_password)) { template::bad($lang['incorrect_login']);
The user class has both static and normal methods. The static ones doesn't require an id to start from, normal methods does. For example user::check(); does check if the username and the password exists in the database.
The template class has just two static methods (template::bad() and template::good()) that manage fast dialog box to send to the user without any header or footer. Instead if you instantiate the class $t = new template('user_view'), the template user_view_body.php is called and you can manage that page with $t->assign() that assign static vars to the template, or with $t->loop() that start a loop etc.
Finally $lang is an array having some common strings in the language set by the user.
# Logging in
$id = user::get_id($user_name, $user_password);
$u = new user($id);
$u->login();
template::good($lang['correct_login']);
At the end we have the actual login. The user class is instantiated and an id is recovered. The user with that id is logged in and we return a template::good() message box to the user.
For any clarification write a comment above.
First of all a message digest function like sha1 is not an encryption function. So please remove crypt from the function name to avoid confusion. Further more, this whole idea of a "custom algorithm" for storing passwords scares the hell out of me. sha256 is a better choice than sha1, but sha1 isn't all that bad after all its still a NIST approved function. Unlike md5 no one has been able to generate a collision for sha1 (although this will happen in the new few years). If you do go with sha1 make sure the salt is a prefix, as to thwart the prefixing attack.
Input validation must always be done at the time of use. the post() function should not be responsible for any input validation or escaping. This is just an incarnation of magic_quotes_gpc which is being removed because its a security train wreak.
Parametrized Query libraries like PDO and ADODB are very good and I recommended using it. This is a great example of sanitizing at the time of use.
Your template assign() method can be used to sanitize for XSS. Smarty comes with a htmlspecialchars output filter module which does this.
With so much functionality behind functions that you haven't provided, it's hard to say whether anything needs to be fixed. Are you making sure the input is clean in post()? Are your database calls secure in user::check()? Are you using session cookies to simplify user prefs/info storage? Is your template class well-written? When you create a new user, are you logging all the necessary information (time of login, IP address if necessary, etc)? Are you ensuring that the user isn't doubly logged in, if that's important here?
The only concrete thing I can tell you about what you wrote is that the sha1 algorithm is completely broken, and you should be using something else instead; see the comments below for suggestions.