1. No Profile Picture
    Contributing User
    Devshed Frequenter (2500 - 2999 posts)

    Join Date
    Dec 2004
    Posts
    2,967
    Rep Power
    374
    Originally Posted by derplumo
    Is the salt every time the same or does the query already take care of that?
    query doesnt take care of salt.. in the query you simply put in $hash variable which you must hash before hand with a salt.

    Whether it is the same or not, depends on you.
  2. --
    Devshed Expert (3500 - 3999 posts)

    Join Date
    Jul 2012
    Posts
    3,957
    Rep Power
    1045
    There is no salt. This is a simple, plain SHA-256 hash.

    You do understand what this is all for, right? If you store the email addresses as plaintext, they'll get leaked and probably spammed in case somebody breaks into your database. To prevent this, you only store the hash of the email address rather than the address itself. This way an attacker will only get a bunch of useless hex strings.

    The hashes do not need to be protected, because it simply makes no sense to attack them. They don't contain any information except "The owner of this email address has once clicked on the 'unsubscribe' link".
    The 6 worst sins of security ē How to (properly) access a MySQL database with PHP

    Why canít I use certain words like "drop" as part of my Security Question answers?
    There are certain words used by hackers to try to gain access to systems and manipulate data; therefore, the following words are restricted: "select," "delete," "update," "insert," "drop" and "null".
  3. Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Feb 2013
    Posts
    368
    Rep Power
    8
    I got an error in this file:
    PHP Code:
    <?php
    $email_adress 
    $_GET["email"];  
    $hash hash('sha256'$email_adress);

    $user_stmt $db->prepare(
            SELECT
                EXISTS (
                    SELECT
                        1
                    FROM
                        unsubscribed_email_addresses
                    WHERE
                        email_address = :hash
                )
        '
    ); 
    $user_stmt->execute(array( 
        
    ':hash' => $hash
    )); 
    $user_id $user_stmt->fetchColumn();
    var_dump($user_id); 
    ?>
    error:

    Call to a member function prepare() on a non-object (line 5)
  4. --
    Devshed Expert (3500 - 3999 posts)

    Join Date
    Jul 2012
    Posts
    3,957
    Rep Power
    1045
    I don't see $db defined anywhere. I guess you forgot to include your common.php.
    The 6 worst sins of security ē How to (properly) access a MySQL database with PHP

    Why canít I use certain words like "drop" as part of my Security Question answers?
    There are certain words used by hackers to try to gain access to systems and manipulate data; therefore, the following words are restricted: "select," "delete," "update," "insert," "drop" and "null".
  5. Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Feb 2013
    Posts
    368
    Rep Power
    8
    Originally Posted by Jacques1
    I don't see $db defined anywhere. I guess you forgot to include your common.php.
    always those little things...
  6. Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Feb 2013
    Posts
    368
    Rep Power
    8
    ok, I got it working, now I only have 1 flaw left, if some joker wants to block someone, he can modify the link by changing the email-adress and then blocking anyone he wants... maybe I could send a random number with the email (in the url) and then controlling it against the table with the email, if those match, block the mail.

    Good idea or not worth trying?
  7. --
    Devshed Expert (3500 - 3999 posts)

    Join Date
    Jul 2012
    Posts
    3,957
    Rep Power
    1045
    Didn't we already discuss that in #213/#214?

    Yes, do use a random number. Your table is gonna need at least three columns:

    • email_key VARCHAR(32) PRIMARY KEY
    • email_address CHAR(64) UNIQUE
    • unsubscribed BOOLEAN DEFAULT 0

    Whenever an email is about to go out to an unregistered address, you first check if there's an entry with this hash and unsubscribed = 1 in the table above. If it is, the address is unsubscribed and shouldn't receive emails. Otherwise, you generate a random email key (in this case 16 bytes), hash the address and store both in the table above. The email key also goes into the email so that the corresponding address can be unsubscribed by clicking on a link.
    The 6 worst sins of security ē How to (properly) access a MySQL database with PHP

    Why canít I use certain words like "drop" as part of my Security Question answers?
    There are certain words used by hackers to try to gain access to systems and manipulate data; therefore, the following words are restricted: "select," "delete," "update," "insert," "drop" and "null".
  8. Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Feb 2013
    Posts
    368
    Rep Power
    8
    We got here the forgot_password, response_forgot_password and the unsubscribe files, in the unsubscribe.php is a fault hidden which I can't find but the var_dump says $unsub: bool(false). Please help, if we can solve this problem, we got the whole scripts I think.

    Here are the scripts:

    forgot_password.php
    PHP Code:
    <?php    
    require("common.php"); 
    require(
    "lib/rnum.php");
    require(
    "lib/mail.php");
    require(
    "lib/password.php"); 
        
    if(!empty(
    $_POST)) 

     if(
    filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) 
     {
        
    $email_adress $_POST["email"];  
        
    $hash hash('sha256'$email_adress);
        
    $user_stmt $db->prepare(
            SELECT
                unsubscribed
            FROM
                unsubscribed_email_addresses
            WHERE
                email_address = :hash
        '
    ); 
        
    $user_stmt->execute(array( 
            
    ':hash' => $hash
        
    )); 
        
    $user_sub $user_stmt->fetchColumn();
        if(
    $user_sub['unsubscribed'] == 1)
        {
            echo 
    "This mail is blocked, please contact us at the contact page.";
        }
        else {
            
    $user_stmt $db->prepare(
                SELECT 
                    id 
                FROM 
                    users 
                WHERE 
                    email = :email 
            '
    ); 
            
    $user_stmt->execute(array( 
                
    ':email' => $_POST['email'
            )); 
            
    $user_id $user_stmt->fetchColumn(); 
                   
            
    $deactivation_stmt $db->prepare(
                UPDATE 
                    responses 
                SET 
                    active = 0 
                WHERE 
                    user = :user 
            '
    ); 
            
    $deactivation_stmt->execute(array( 
                
    ':user' => $user_id 
            
    )); 
            
            
    $email $_POST['email'];
            
    $password_token rnum();
     
             
    $query 
            INSERT INTO responses ( 
            reset_key,
            user,
            secret,
            request_timestamp,
            request_ip
            ) VALUES ( 
            :reset_key,
            :user,
            :secret,
            NOW(),
            :request_ip
            )"
    ;
     
            
    $secret password_hash($password_tokenPASSWORD_BCRYPT, array("cost" => 10));  
            
    $reset_key rnum();
            
    $user $user_id['id'];
            
    $request_ip getenv('REMOTE_ADDR');

            
    $query_params = array( 
                
    ':reset_key' => $reset_key,
                
    ':user' => $user,
                
    ':secret' => $secret,
                
    ':request_ip' => $request_ip
            
    ); 
                    
            
    $stmt $db->prepare($query); 
            
    $result $stmt->execute($query_params); 
            

            
    $email_key rnum();
            
    $email_hash hash('sha256'$email_adress);
            if(
    $user_sub)
            {
            
    $unsubscribe_stmt $db->prepare(
                INSERT INTO unsubscribed_email_addresses ( 
            email_key,
            email_address
            ) VALUES ( 
            :email_key,
            :email_address
            )
            '
    ); 
            
    $unsubscribe_stmt->execute(array( 
                
    ':email_key' => $email_key,
                
    ':email_address' => $email_hash
            
    )); 
            }
            
            
    $url 'http://www.domain.com/response_forgot_password.php?reset_key=';
            
    $url .= $reset_key;
            
    $url .= '&user=';
            
    $url .= $user;
            
    $url .= '&password_token=';
            
    $url .= $password_token;

            
    $url_2 'http://www.domain.com/unsubscribe.php?email_key=';
            
    $url_2 .= $email_key;
            
    $url_2 .= '&email_address=';
            
    $url_2 .= $email_hash;
                
            
    $mail_to      $email;
            
    $mail_subject 'Forgot password';
            
    $mail_body 'Click this link to reset your password:<br><br>';
            
    $mail_body .= $url;

            
    $mail_body .= "<br><br> If you don't wish to receive any e-mails from us again, please click this link:<br><br>";
            
    $mail_body .= $url_2;
            
    mail_f ($mail_to$mail_subject$mail_body);
            echo 
    'An email with further instruction has been sent to ' htmlentities($_POST['email'], ENT_QUOTESENT_HTML401'UTF-8'); 
        }
    }
    else{
     echo 
    "This emailadress is invalid!";
    }               
    }

    ?> <html>
    <body>
    <form action="forgot_password.php" method="post"> 
        Email:
        <input type="text" name="email" value="" /> 
        <br /><br /> 
        <input type="submit" value="Login" /> 
    </form> 
    </body>
    </html>
    response_forgot_password.php
    PHP Code:
    <?php   
    require("common.php");   
    require(
    "lib/password.php");  


        
    $reset_key $_GET["reset_key"];  
        
    $user $_GET["user"];  
        
    $password_token $_GET["password_token"];  
          

        if(!empty(
    $_POST))   
        {   
    $reset_echo true;
    $reset_successful false
            
    $reset_key $_POST['reset_key'];  
            
    $user $_POST['user'];  
            
    $password_token $_POST['password_token'];  

            
    $query "   
                SELECT   
                    user  
                    , secret 
                    , request_timestamp 
                FROM 
                    responses  
                WHERE   
                    reset_key = :reset_key 
                    AND user = :user 
                    AND NOT used 
                    AND active 
                "

               
            
    $query_params = array(   
                
    ':reset_key' => $reset_key
                
    ':user' => $user  
            
    );   
               
             
            
    $stmt $db->prepare($query);   
            
    $result $stmt->execute($query_params);   
              
            
    $row $stmt->fetch();   

            if(
    $row)  
            {              
                        
    $created DateTime::createFromFormat('Y-m-d G:i:s'$row['request_timestamp']);   
                        if ( 
    $created >= new DateTime('30 minutes ago') )   
                        {  
                            if ( 
    password_verify($password_token$row['secret']) )  
                            {  
                                   
    $query "   
                                UPDATE users   
                                SET   
                                    password = :password  
                                WHERE  
                                id = :id   
                                "
    ;   
                  
                                  
    $hash password_hash($_POST['password'], PASSWORD_BCRYPT, array("cost" => 10));           
               
                                
    $query_params = array(   
                                    
    ':password' => $hash,   
                                    
    ':id' => $row['user']  
                                );   
               
               
                                    
    $stmt $db->prepare($query);   
                                
    $result $stmt->execute($query_params);   
    $reset_successful true
                            }   
                        }         
            }   
        }  
    ?> <html>
    <body>
    <?php  
    if ($reset_echo) {
    if (
    $reset_successful
        echo 
    'Your password has been reset!'
    else
        echo 
    'This token has already been used, expired or inactive, please request a new one';  
    }
    ?>
    <form action="response_forgot_password.php" method="post">    
     New password:  
        <input type="text" name="password" value="" />   
        <br /><br />   
    <input type="hidden" name="reset_key" value="<?php echo $reset_key?>" />  
    <input type="hidden" name="user" value="<?php echo $user?>" />  
    <input type="hidden" name="password_token" value="<?php echo $password_token?>" />  
        <input type="submit" value="Login" />   
    </form>   
    </body>
    </html>
    unsubscribe.php
    PHP Code:
    <?php
    require("common.php");
    $email_key $_GET["email_key"];  
    $email_address $_GET["email_address"];  
    if(!empty(
    $email_address))  
    {          
            
    $hash hash('sha256'$email_adress);
            
    $user_stmt $db->prepare('  
            SELECT *
            FROM
                unsubscribed_email_addresses
            WHERE
                email_key = :email_key
            
            '
    ); 
            
    $user_stmt->execute(array(  
                
    ':email_key' => $email_key
            
    ));  
            
    $unsub $user_stmt->fetchColumn();         
            
    var_dump($unsub); 
            
            if(
    $hash == $unsub['email_address'] && $unsub['unsubscribed'] == 0)
            {
                
    $unsubscribe_stmt $db->prepare(
                   UPDATE  
                    unsubscribed_email_addresses
                SET  
                    unsubscribed = 1  
                WHERE  
                    email_key = :email_key  
                '
    ); 
                
    $ubsubscribe_stmt->execute(array( 
                    
    ':email_key' => $email_key
                
    )); 
            }    
    }
    echo 
    "This email has been unsubscribed.";
    ?>
    And I have a comment on the original tutorial, the email-addresses are stored in plaintext, shouldn't we hash it like in the unsubscribe part of the response_forgot_password? Like you said Jacques, if it would be a political site, I would not like someone to know if I'm registered.

    Yes of course someone who can break into a database or server can also find out your address from all those hashes, but it buys time, or at least some effort...
    Last edited by derplumo; May 4th, 2013 at 05:57 PM.
  9. --
    Devshed Expert (3500 - 3999 posts)

    Join Date
    Jul 2012
    Posts
    3,957
    Rep Power
    1045
    The email logic is still flawed. Currently, you use the unsubscription mechanism for any email adress, so a registered user could unsubscribe their own address and would no longer be able to reset the password or receive any other emails. This obviously makes no sense. Unsubscription is meant for unregistered email addresses only. It's used to protect "innocent people" that have nothing to do with your site and might find the emails irritating and annoying.

    The procedure looks like this:



    Remove the error message for blocked addresses. Your form shouldn't give out any info about any email address. Just put a general explanation on the feedback page: "We will send you an email with further instructions. But if your address does not exist in our system and you have unsubscribed it earlier, we will not send a mail."

    You're also using the database fetch methods wrong. fetchColumn() does exactly what the name says: It fetches a single column. You don't have access to the other columns. If you want the full row, you need fetch().

    The bool(false) indicates that your PDO error reporting is misconfigured. What does your PDO initialization look like? In the meantime, simply dump the email key and execute the query manually in phpmyadmin. This will tell you what's wrong.



    Originally Posted by derplumo
    And I have a comment on the original tutorial, the email-addresses are stored in plaintext, shouldn't we hash it like in the unsubscribe part of the response_forgot_password? Like you said Jacques, if it would be a political site, I would not like someone to know if I'm registered.
    I think this is a misunderstanding. If you hash something, the original data gets lost. Hashing an email address means you're left with some hex string, and you cannot get the original address back (a hash is one-way). So you cannot send emails to your users -- but wasn't that the whole point of asking for the email address?

    The reason why hashes work in the unsubscription process is because you do not need to actually know the addresses. All you want is check if a given address exists in your system. In other words, you merely need a "fingerprint" of the address rather than the address itself. A hash is perfect for that. But if you need the actual address for sending emails to it, you cannot use a hash.

    What you can do, however, is completely separate the email addresses from your public application and have all email processing be done by isolated cron jobs. This way the addresses will be safe from attacks on your website. That is, even if somebody manages to do an SQL injection or similar attacks, they still don't have access to the email addresses.

    However, this is a different topic, so we should discuss it in a new thread.
    Attached Images
    Last edited by Jacques1; May 5th, 2013 at 01:38 PM.
    The 6 worst sins of security ē How to (properly) access a MySQL database with PHP

    Why canít I use certain words like "drop" as part of my Security Question answers?
    There are certain words used by hackers to try to gain access to systems and manipulate data; therefore, the following words are restricted: "select," "delete," "update," "insert," "drop" and "null".
  10. Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Feb 2013
    Posts
    368
    Rep Power
    8
    Jacques, we're close to complete the scripts, I re-edited the forgot_password to suit the nice model you made. I only have one error at this file:

    unsubscribe.php
    PHP Code:
    <?php
    require("common.php");
    $email_key $_GET["email_key"];  
    $email_address $_GET["email_address"];  
    if(!empty(
    $email_address))  
        {          
            
    $user_stmt $db->prepare('  
            SELECT *
            FROM
                unsubscribed_email_addresses
            WHERE
                email_key = :email_key
            
            '
    ); 
        
    $user_stmt->execute(array(  
            
    ':email_key' => $email_key
        
    ));  
        
    $unsub $user_stmt->fetch();  

            if(
    $email_address == $unsub['email_address'] && $unsub['unsubscribed'] == 0)
            {

                
    $unsubscribe_stmt $db->prepare(
                   UPDATE  
                    unsubscribed_email_addresses
                SET  
                    unsubscribed = 1  
                WHERE  
                    email_key = :email_key  
                '
    ); 
                
    $ubsubscribe_stmt->execute(array( 
                    
    ':email_key' => $email_key
                
    ));         
            }    
    }
    echo 
    "This email has been unsubscribed.";
    ?>
    the error is:

    Fatal error: Call to a member function execute() on a non-object (line 35)

    The fault we had earlier was that the key in the mail didn't match the one in the table...
    Last edited by derplumo; May 6th, 2013 at 03:12 PM.
  11. --
    Devshed Expert (3500 - 3999 posts)

    Join Date
    Jul 2012
    Posts
    3,957
    Rep Power
    1045
    $unsubscribe != $usubscribe. You need a proper IDE like Netbeans or Eclipse. This will notify you of typos and other obvious mistakes.

    Do not pass the email address (or its hash) in the URL. The email key already is a unique identifier pointing to exactly one email address.

    The last problem is that you're outputting "email has been unsubscribed" in any case, even if it has not been updated. You should also get rid of the SELECT query. Just do the UPDATE and check the number of affected rows afterwards. This has two advantages: You only need one query. And you actually check whether the query was sucessful (and not only if the record exists).

    PHP Code:
    <?php

    require("common.php");


    if( !empty(
    $_GET['email_key']) )
    {
        
    $unsub_stmt $db->prepare('
            UPDATE
                unsubscribed_email_addresses
            SET
                unsubscribed = 1
            WHERE
                email_key = :email_key

        '
    );
        
    $unsub_stmt->execute(array(
            
    ':email_key' => $_GET['email_key']
        ));
        if ( 
    $unsub_stmt->rowCount() )    // number of affected rows
            
    echo 'Your email address has been unsubscribed. You will no longer receive notification emails.';
        else
            echo 
    'Invalid email key. Please check the URL in your previous notification email.';
    }
    else
        echo 
    'Missing email key. Please check the URL in your previous notification email. It must have the form http://yourpage.com/unsubscribe.php?email_key=...';
    Actually, "unsubscription" is kind of a bad term for this, because it implies that the user has somehow subscribed before -- which is of course not the case. This might be irritating. If you come up with a better term than I did, use it.
    The 6 worst sins of security ē How to (properly) access a MySQL database with PHP

    Why canít I use certain words like "drop" as part of my Security Question answers?
    There are certain words used by hackers to try to gain access to systems and manipulate data; therefore, the following words are restricted: "select," "delete," "update," "insert," "drop" and "null".
  12. No Profile Picture
    Registered User
    Devshed Newbie (0 - 499 posts)

    Join Date
    May 2013
    Posts
    1
    Rep Power
    0
    i keep getting this error
    Fatal error: Class 'PDO' not found in /var/www/localhost/htdocs/common.php on line 15

    this is the line that is failing.

    $options = array(PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8');
  13. Did you steal it?
    Devshed Supreme Being (6500+ posts)

    Join Date
    Mar 2007
    Location
    Washington, USA
    Posts
    13,959
    Rep Power
    9397
    Originally Posted by jimbobjoe42
    i keep getting this error
    Fatal error: Class 'PDO' not found in /var/www/localhost/htdocs/common.php on line 15
    To use PDO you have to install it first.
  14. Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Feb 2013
    Posts
    368
    Rep Power
    8
    Ok, but still, why don't we do it like this (attachment)? This way people outside the website don't get annoying mails from it, and nobody can see if an emails has been dispatched...

    And in the unsubscribe file I get this echo when all matches and the unsubscribe is set to 1:

    Invalid email key. Please check the URL in your previous notification email.
    Attached Images
  15. --
    Devshed Expert (3500 - 3999 posts)

    Join Date
    Jul 2012
    Posts
    3,957
    Rep Power
    1045
    Originally Posted by derplumo
    Ok, but still, why don't we do it like this (attachment)? This way people outside the website don't get annoying mails from it, and nobody can see if an emails has been dispatched...
    This means that your users don't get any response if the email address is wrong. If they make a small typo or mix up their various email addresses, they can wait forever.

    It also means that your form cannot give any feedback at all. You mustn't say anything like "We've sent you an email", because this could very well be a lie. In other words: Your form would have to be completely silent.

    Prepare for some angry users telling you that your password reset is broken, because they've waited 12 hours and still received no email -- or they simply don't come back, depending on how important they find your site.

    So, no, this doesn't sound like a good approach. If you're super-worried about sending out unwanted emails, you can limit the number to 1 in 24 hours.



    Originally Posted by derplumo
    And in the unsubscribe file I get this echo when all matches and the unsubscribe is set to 1:

    Invalid email key. Please check the URL in your previous notification email.
    Yep, that's a problem.

    PHP Code:
    <?php

    require("common.php");


    if( !empty(
    $_GET['email_key']) )
    {
        
    $key_stmt $db->prepare('
            SELECT
                unsubscribed
            FROM
                unsubscribed_email_addresses
            WHERE
                email_key = :email_key
        '
    );
        
    $key_stmt->execute(array(
            
    ':email_key' => $_GET['email_key']
        ));
        
    $unsub $key_stmt->fetchColumn();
        if ( 
    $unsub === false )            // key doesn't exist
            
    echo 'Invalid email key. Please check the URL in your previous notification email.';
        else                            
    // key exists
        
    {
            if ( !
    $unsub )                // not unsubscribed yet
            
    {
                
    $unsub_stmt $db->prepare('
                    UPDATE
                        unsubscribed_email_addresses
                    SET
                        unsubscribed = 1
                    WHERE
                        email_key = :email_key
                '
    );
                
    $unsub_stmt->execute(array(
                    
    ':email_key' => $_GET['email_key']
                ));
                if ( 
    $unsub_stmt->rowCount() )
                    
    $unsub true;
            }
            if ( 
    $unsub )
                echo 
    'Your email address has been unsubscribed. You will no longer receive notification emails.';
            else
                echo 
    'There was a technical issue. Please try again later.';
        }
    }
    else
        echo 
    'Missing email key. Please check the URL in your previous notification email. It must have the form http://yourpage.com/unsubscribe.php?email_key=...';
    The 6 worst sins of security ē How to (properly) access a MySQL database with PHP

    Why canít I use certain words like "drop" as part of my Security Question answers?
    There are certain words used by hackers to try to gain access to systems and manipulate data; therefore, the following words are restricted: "select," "delete," "update," "insert," "drop" and "null".

IMN logo majestic logo threadwatch logo seochat tools logo