어떤 application이든 패스워드를 사용하게 마련이다. 하지만, 사용자들은 패스워드를 자주 잃어버리기 때문에 재설정을 하기 위한 방법을 제공한다.
email을 이용하나 패스워드 복구 방법에서 사용자가 가장 많이 실수하는 것은 패스워드를 그대로 email로 보내주는 것이다.
이것은 보안에 심각한 문제를 담고 있다.
다음과 같은 테이블이 있다고 가정하자.
CREATE TABLE Accounts (
account_id SERIAL PRIMARY KEY,
account_name VARCHAR(20) NOT NULL,
email VARCHAR(100) NOT NULL,
password VARCHAR(30) NOT NULL
);
여기에 행을 삽입하여 계정을 생성할 수 있다.
INSERT INTO Accounts (account_id, account_name, email, password)
VALUES (123, 'billkarwin', 'bill@example.com', 'xyzzy');
위와 같이 사용하는 경우 다음과 같은 위험이 있다.
사용자가 로그인을 시도할 때, 애플리케이션은 사용자의 입력과 데이터베이스에 저장된 패스워드를 비교하는데 일반적으로 다음과 같은 쿼리를 사용한다고 가정하자.
SELECT CASE WHEN password = 'opensesame' THEN 1 ELSE 0 END
AS password_matches
FROM Accounts
WHERE account_id = 123;
위 쿼리는 두개의 값이 같으면 1 아니면 0을 return한다.
이와 같이 사용하는 경우 앞에서 얘기한것과 같이 공격자에게 패스워드가 노출될 위험이 증가한다.
참고
2개의 다른 조건을 한 덩어리로 만들지 마라.
일반적으로 다음과 같이 사용하는 경우가 많다.
SELECT * FROM Accounts
WHERE account_name = 'bill' AND password = 'opensesame';
이와 같이 사용하는 경우 account name이 달라서 빈 결과를 나타내는 것인지 패스워드가 달라서 빈 결과를 나타내는 것인지 확인할 수 없으므로, 정확한 인증 실패 원인을 파악할 수 없다.
패스워드가 평문으로 저장되기 때문에 패스워드를 검색하는 것도 다음과 같이 간단히 처리할 수 있다.
SELECT account_name, email, password
FROM Accounts
WHERE account_id = 123;
이렇게 나온 결과를 e-mail을 통해 전달하기도 하는데 이와 같은 경우 공격자가 가로채서 따로 저장할 가능성이 높다.
다음과 같은 경우가 안티패턴이 된다.
다음과 같은 경우에 사용이 합당하다.
다음과 같은 방법을 사용하여 좀더 안전하게 패스워드를 사용하도록 하자.
해시 함수를 사용하여 암호를 encode한다.
안전하게 사용하기 위해서는 다음과 같은 알고리즘을 사용하는 것이 좋다.
다음은 account 테이블을 다시 정의 한 것이다. SHA-256을 사용한다고 가정한 경우이다.
CREATE TABLE Accounts (
account_id SERIAL PRIMARY KEY,
account_name VARCHAR(20),
email VARCHAR(100) NOT NULL,
password_hash CHAR(64) NOT NULL
);
INSERT INTO Accounts (account_id, account_name, email, password_hash)
VALUES (123, 'billkarwin', 'bill@example.com', SHA2('xyzzy'));
SELECT CASE WHEN password_hash = SHA2('xyzzy') THEN 1 ELSE 0 END
AS password_matches
FROM Accounts
WHERE account_id = 123;
공격자가 dictionary attack하는 것을 막기위해 암호를 encode할 때 salt를 추가한다. salt는 해시 값을 구하기 전에 사용자의 패스워드에 덧붙이는 무의미한 바이트열을 의미한다.
이와 같이 사용하는 경우 사용자가 사전에 있는 단어를 사용하더라도, 공격자가 풀어낼 수 없게 할 수 있다.
패스워드 마다 다른 소금값을 사용하는 경우 salt 정보를 가지고 있어야 한다.
CREATE TABLE Accounts (
account_id SERIAL PRIMARY KEY,
account_name VARCHAR(20),
email VARCHAR(100) NOT NULL,
password_hash CHAR(64) NOT NULL,
salt BINARY(8) NOT NULL
);
INSERT INTO Accounts (account_id, account_name, email,
password_hash, salt)
VALUES (123, 'billkarwin', 'bill@example.com',
SHA2('xyzzy' || '-0xT!sp9'), '-0xT!sp9');
SELECT (password_hash = SHA2('xyzzy' || salt)) AS password_matches
FROM Accounts
WHERE account_id = 123;
사용하는 SQL문장에서도 패스워드가 바로 노출되지 않도록 해야 한다.
SQL 쿼리에 패스워드를 평문으로 넣지 않고 대신 애플리케이션 코드에서 해시 값을 계산해 SQL 쿼리에서 이 해시 값만 사용하면, 이런 종류의 노출을 피할 수 있다.
다음은 PHP 코드이다.
<?php
$password = 'xyzzy';
$stmt = $pdo->query(
"SELECT salt
FROM Accounts
WHERE account_name = 'bill'");
$row = $stmt->fetch();
$salt = $row[0];
$hash = hash('sha256', $password . $salt);
$stmt = $pdo->query("
SELECT (password_hash = '$hash') AS password_matches;
FROM Accounts AS a
WHERE a.account_name = 'bill'");
$row = $stmt->fetch();
if ($row === false) {
// account 'bill' does not exist
} else {
$password_matches = $row[0];
if (!$password_matches) {
// password given was incorrect
}
}
또한, 사용자가 패스워드를 입력하고 인증을 받으려고 할 때 평문상태로 보내게 되는데 이때 패스워드가 노출되는 위험이 있게 된다. 이 부분에서 문제를 보완하기 위해서는 https와 같은 보안 프로토콜을 사용하는 것이 좋다.
패스워드를 잃어버린 경우 복구를 하는 것보다 임시 패스워드를 전달하여 재설정하게 유도하는 것이 좋다.
아니면, 요청 내용을 기록하여 그 사람만 접근할 수 있는 변경 url을 메일을 통해 전달하는 것이다.