Статьи

Рекомендации по работе с конфиденциальными данными: защита вашего приложения

В моей предыдущей статье я показал вам, как защитить свой сервер от атак и вредоносного программного обеспечения. Эта часть будет полностью сосредоточена на третьем уровне безопасности — самом приложении. Итак, здесь я покажу вам методы, которые вы можете использовать для защиты своего приложения от атак и вторжений.

При связи с базой данных и для обеспечения безопасности ваших данных, помните о следующих ключевых моментах:

Чтобы запретить злоумышленникам использовать SQL-инъекцию , необходимо скрыть ввод всех пользователей, чтобы они не могли внедрять SQL-запросы в ваше приложение (например, во время входа в систему). Практически во всех драйверах базы данных для всех языков есть возможность избежать пользовательского ввода. Например, в node-mysql для Node.js, вместо выполнения ваших запросов следующим образом:

1
connection.query(‘SELECT * FROM users WHERE name = \»+ username +’\’ AND password = \»+ password ‘\’;’, function (err, rows, fields) {

Вы можете автоматически избежать их, используя этот синтаксис:

1
connection.query(‘SELECT * FROM users WHERE name = ? AND password = ?;’, [ username, password ], function (err, rows, fields) {

Переменные из массива, предоставленные в качестве второго аргумента метода query будут вставлены вместо знаков вопроса в строке запроса и будут автоматически экранированы. То же самое можно сделать в PHP, используя подготовленные операторы PDO:

1
2
3
4
5
$stmt = $dbh->prepare(«SELECT * FROM users WHERE name = ? AND password = ?;»);
$stmt->bindParam(1, $username);
$stmt->bindParam(2, $password);
 
$stmt->execute();

Даже если вы избегаете запросов к базе данных, вы никогда не узнаете, пропустила ли ошибка в вашем приложении какой-либо вредоносный код. Если вы написали свое приложение так, что вы всегда предполагаете, что содержимое в базе данных безопасно, то ваши пользователи подвергаются опасности. Например, этот код:

1
2
3
4
5
6
7
if ($result = $mysqli->query(‘SELECT * FROM users WHERE name = «‘. $mysqli->escape_string($username) .’»;’)) {
    if ($row = $result->fetch_assoc()) {
        if ($result = $mysqli->query(‘SELECT * FROM someData WHERE uid = ‘. $row[‘id’] .’;’)) {
            …
        }
    }
}

Небезопасно! Если что-то пойдет не так, и идентификатор пользователя содержит некоторый код SQL-инъекции, у вас будут проблемы. $row['id'] также следует экранировать с помощью escape_string() , например:

1
2
3
4
5
6
7
if ($result = $mysqli->query(‘SELECT * FROM users WHERE name = «‘. $mysqli->escape_string($username) .’»;’)) {
    if ($row = $result->fetch_assoc()) {
        if ($result = $mysqli->query(‘SELECT * FROM someData WHERE uid = ‘. $mysqli->escape_string($row[‘id’]) .’;’)) {
            …
        }
    }
}

А теперь представьте себе такую ​​ситуацию: некоторые гениальные хакеры взломали вашу тщательно созданную систему безопасности и взялись за вашу базу данных. Затем они начинают перебор всех паролей ваших пользователей. Если вы использовали соль при хешировании, вы можете спокойно спать и уверять своих пользователей в безопасности своих данных. Потому что, если у злоумышленника нет квантового компьютера в подвале, ему понадобятся годы, чтобы взломать любой из паролей.

Использование соли означает, что вы добавляете несколько случайных символов к паролю перед хэшированием (эти символы называются солью) и сохраняете их с паролем. Вы можете подумать, что это не очень безопасно, поскольку злоумышленник узнает соль, если получит доступ к базе данных. Но так как соль различна для каждого пользователя, даже если два из них используют один и тот же пароль, их хэши будут разными. Это заставляет атакующего взломать их один за другим, что отнимает много времени и обычно не стоит потраченного времени. Это также означает, что злоумышленник не может использовать радужные таблицы или словарь для поиска паролей.

Сначала вы должны генерировать соль. Например, вы можете использовать crypto.randomBytes() :

1
var crypto = require(‘crypto’);
1
2
3
4
5
6
/*
    this should be done on request with the user’s data for registration,
    instead of just adding it to the database
*/
 
crypto.randomBytes(16, function (e, salt) {

Эта функция выдаст ошибку, если, согласно документации Node.js , «энтропии недостаточно для генерации криптографически стойких данных». В таком случае вам следует либо повторить попытку, либо использовать crypto.pseudoRandomBytes() , который генерирует не криптографически сильные случайные байты, но, поскольку это будет происходить только изредка, мы можем использовать его:

1
2
3
if (e) {
       salt = crypto.pseudoRandomBytes(16);
   }

Теперь давайте добавим данные. Для простоты я буду использовать только имя пользователя, пароль и соль в этом примере:

1
2
3
4
5
6
/* password and username should be extracted from request body */
    var hash = crypto.createHash(‘sha-256’).update(password + salt.toString()).digest(‘hex’);
    sql.query(‘INSERT INTO users VALUES(?, ?, ?);’, [ username, password, salt ], function (err, rows, fields) {
        /* your other app logic here, what happes after the user data is into the database */
    });
});

Когда ваш пользователь хочет войти в систему, вы должны получить соль из базы данных, а затем проверить пароль:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
sql.query(‘SELECT salt FROM users WHERE name = ?;’, [ username ], function (err, rows, fields) {
    if (rows.length < 1) {
        /* the user does not exist */
    } else {
        var hash = crypto.createHash(‘sha-256’).update(password + rows[0].salt).digest(‘hex’);
        sql.query(‘SELECT 1 FROM users WHERE name = ? AND password = ?;’, [ username, hash ], function (err, rows, fields) {
            if (rows.length < 1) {
                /* wrong password */
            } else {
                /* password ok */
            }
        });
    }
});

И это в значительной степени так. Теперь давайте посмотрим, как мы можем сделать это с помощью PHP.

В PHP это намного проще, потому что встроенные функции password_hash() и password_verify() автоматически используют соль, а возвращаемый хеш содержит ее, поэтому вам даже не нужен еще один столбец в базе данных (или поле в случае NoSQL) , Это будет выглядеть примерно так:

1
2
3
4
5
6
$stmt = $dbh->prepare(«INSERT INTO users VALUES (?, ?);»);
$stmt->bindParam(1, $username);
/* hash the password, the function adds random salt automatically */
$stmt->bindParam(2, password_hash($password));
 
$stmt->execute();

Чтобы проверить логин, вам просто нужно вызвать password_verify() . Он автоматически получает соль из хэша, поэтому вам нужно только указать хеш из вашей базы данных и пароль в виде простого текста для сравнения. Необходимый код выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
$stmt = $dbh->prepare(«SELECT password FROM users WHERE name = ?;»);
$stmt->bindParam(1, $username);
 
$stmt->execute();
 
$row = $stmt->fetch(PDO::FETCH_ASSOC);
 
if ($row) {
    /* check if the password matches */
    if (password_verify($password, $row[‘password’])) {
        /* password ok!
    }
} else {
    /* the user does not exist */
}

Это один из лучших защитных механизмов, которые вы можете использовать для защиты своей машины и, следовательно, ваших пользователей. Вам нужны повышенные привилегии для пары вещей, в основном для прослушивания номеров портов ниже 1024 или работы с системными файлами. Но вам никогда не следует запускать какое-либо приложение от имени пользователя root, когда вам это не нужно, на случай, если злоумышленник обнаружит в вашем коде ошибку, позволяющую ему выполнять команды на вашем сервере, и если код выполняется как привилегированный пользователь, игра окончена. , Злоумышленник может делать все, что ему захочется, и, вероятно, это будет сделано до того, как вы заметите что-либо Вот почему вы должны отказаться от привилегий как можно быстрее. В Node.js это будет выглядеть так:

1
2
3
4
5
6
var app = http.createServer(…);
 
 
app.listen(80);
process.setuid(‘app_user’);

Функция process.setuid() изменит идентификатор пользователя процесса на тот, который ему передан — это может быть либо числовой идентификатор, либо строка имени пользователя (во втором случае эта функция будет блокироваться при получении идентификатора пользователя). ). Пользователь должен быть непривилегированным и иметь доступ только к файлам приложения, чтобы ограничить риск предоставления злоумышленнику доступа ко всему другому на вашей машине.

Некоторые люди говорят (например, в этом комментарии об удалении привилегий root ), что приведенное выше решение не идеально, и вместо этого они предпочитают использовать authbind . Это ваше решение, какое вы выберете, и это в значительной степени зависит от ваших настроек. Но в любом случае, authbind — это команда, которая была разработана для этой цели — она ​​позволяет вашему приложению связываться с портами ниже 1024 без привилегий суперпользователя (так что она охватывает только этот сценарий). Чтобы использовать его, сначала создайте файл: /etc/authbind/byport/port , где port — это номер /etc/authbind/byport/port , к которому вы хотите привязаться, и сделайте его исполняемым пользователем, которого вы будете использовать для запуска вашего приложения. Затем переключитесь на своего пользователя и запустите приложение следующим образом:

1
authbind node yourapp.js

Или как это из корня:

1
su -c ‘authbind node yourapp.js’ youruser

Благодаря этому вы можете достичь той же конечной цели, что и в решении POSIX, просто используя вместо этого authbind , если вы этого предпочитаете.

Надеюсь, что вы выбрали новые методы для работы с конфиденциальными данными в этих двух статьях. В предыдущей статье мы начали с изучения того, как защитить наши данные на сервере, выбрав правильного поставщика сервера, обновив нашу ОС, защитив порты и используя антивирус. В этой статье мы завершили наше обсуждение разговорами о защите самого приложения за счет использования надлежащей защиты базы данных, защиты от перебора паролей и привилегий пользователя.

Не стесняйтесь поделиться своими собственными советами по безопасности в комментариях и спасибо за чтение.