First of all, the passwords are stored safely (assuming the used hashing algorithm is safe) since the only plaintext thing is the salt which has to be plaintext - its only purpose is to prevent the final hash to be
hash(password) since that would allow the usage of precomputed rainbow tables mapping hashes to passwords.
However, the way of storing them using two separate columns is not optimal. While somewhat easy to use when just passing salt and plaintext password to a hash function, most password hashing libraries use a better scheme. Let's have a look a
bcrypt (or rather its python bindings for the sake of simplicity) which uses the same scheme as
>>> import bcrypt >>> bcrypt.gensalt() '$2a$12$xYkBJKIdbmAZlhpl96dopO' >>> bcrypt.hashpw('foo', '$2a$12$xYkBJKIdbmAZlhpl96dopO') '$2a$12$xYkBJKIdbmAZlhpl96dopOJug5oZcp4tVWJPug/xMdFQzVMJDOvH6' >>> bcrypt.hashpw('foo', '$2a$12$xYkBJKIdbmAZlhpl96dopOJug5oZcp4tVWJPug/xMdFQzVMJDOvH6') '$2a$12$xYkBJKIdbmAZlhpl96dopOJug5oZcp4tVWJPug/xMdFQzVMJDOvH6'
As you can see, the resulting hash consists of multiple partsmultiple parts. First the
2a indicating the algorithm, then the "cost" used by the algorithm, then the 22-character salt (in plain text), then the actual hash.
The advantage of this is that checking the password can be done using the same function you use for hashing it - you just pass the full hash string as the salt and then check if it returns the same string. Another, more relevant, advantage is the fact that by storing it like this you can easily write a function to handle various hash algorithms by checking the first field. This is relevant when you have to deal with legacy passwords that were hashed e.g. using MD5 or SHA-1.