Хранение сессии на клиенте

Как вы, должно быть, знаете, хранить пользовательские данные на клиентской стороне плохо: их ведь может подделать злостный хакер. Но сегодня я вам расскажу, как можно безопасно хранить сессионные данные, не волнуясь за их сохранность. Расскажу на примере Snap Framework, на котором и написан этот блог.

Суть метода — шифрование (кто бы мог подумать). Snap для своих сессий использует библиотеку Web.ClientSession, а поэтому что и как обрабатывается можно смело подсмотреть в документации:

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

Skein — это просто хороший алгоритм хеширования. К сожалению, он не победил в конкуре на замену SHA-2, и название SHA-3 получил другой алгоритм. Поэтому самое время заменить Skein на новоявленный SHA-3 — Keccak.

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

В общем, идея имеет право на жизнь. Особенно на небольших, как в моем случае, объемах хранимых данных. Лично я храню только одно поле: является ли текущий пользователь админом или нет.

Хозяйке на заметку: чтобы вашу сессию нельзя было увести с помощью JavaScript, добавьте в заголовок Set-Cookie параметр HttpOnly. Куки с таким параметром не появляются в document.cookie.

Осталась одна загадка, связанная с моим сайтом. Кука _session передается как сессионная и должна удаляться с закрытием браузера. Так и происходит в Opera, но не Google Chrome и Firefox. Может есть какая-то стандартизированная или неофициальная фича, которая не дает сессионным кукам удалиться?

Выполнение пользовательского кода в nodejs

Мне тут по долгу службы пришлось столкнуться с node.js. Это такой серверный JavaScript, если что. И в его основе лежит V8 — JS-движок от Google, который стоит в Google Chrome.

Весьма вероятно, что в ближайшее время вы увидите еще много постов про node.js в этом блоге, а пока вот вам практическая задача.

У нас есть сервис с некоторым набором данных, неважно каких. Этот сервис позволяет выполнять пользовательские операции над этими данными (например, валидацию). Т.е. любой пользователь может зайти, написать что-то вроде setValid(data.a == 1) — и код будет выполнен. Понятное дело, что сразу возникает вопрос безопасности. Ведь никто не мешает пользователю подключить библиотеку работы с файловой системой и сделать что-нибудь нехорошее:

var fs = require('fs');
fs.readdir('~', function(err, files) {
  files.forEach(function (file) {
    fs.unlink('~/' + file);
  });
});

Или может вообще не возвращать результат, а загрузить процессор на 100%. Мы ведь ничего не можем гарантировать, когда дело доходит по пользовательского кода. Да ведь там могут быть и просто синтаксические ошибки.

Итак, мое решение.

Во-первых, весь пользовательский код выполняется в отдельном процессе. Этот процесс принудительно завершается по истечении некоторого времени (секунд 10 или около того).

// parent.js
var child = child_process.fork('./child.js');
child.on('message', function(message) {
    // message будет содержать ответ от потомка
});
setTimeout(function () {
    child.kill();
    console.log('Task was killed by timeout');
}, 10000);
child.send(data); // Оправляем параметры для выполнения
// child.js
process.on('message', function (message) {
    // message будет содержать параметры выполнения
    // TODO ????
    process.send(result); // Отправляем результат
});

Во-вторых, я оборачиваю вызов eval в try..catch, хотя да, это очевидно.

В-третьих, eval вызывается в специальной функции с кучей неиспользуемых параметров. Имена этих параметров соответствуют всем доступным глобальным именам и они будут равны undefined в момент работы eval.

var libs = { // Разрешенные библиотеки
    "libxmljs": require("libxmljs") 
};
var l = "";
for (var i in libs) {
    if (libs.hasOwnProperty(i)) {
        if (l) {
            l += ',';
        }
        l += i + '=this.libs.' + i;
    }
}
l = "var " + l + ';';

// Класс ответственный за выполнение кода
var RunnerClass = function (code) {
    this.code = code;
};
RunnerClass.prototype.__proto__ = EventEmitter.prototype;

RunnerClass.prototype.run = function (data) {
    var code = this.code;

    // Объект, который представляет собой контекст this для выполняемого кода. всё, что описано тут, доступно во вложенном коде.
    var context = new EventEmitter();
    context["data"] = data;
    context["libs"] = libs;
    context["finish"] = function () {
        this.emit("finish");
    };
    context.on("finish", function () {
        this.emit("finish", context.result);
        console.log('Result', context.result);        
    }.bind(this));

    // Вот эти параметры и прячут глобальные переменные
    var sandbox = function(Buffer, CodeRunner, EventsEmitter, context, global, exports, i, libs, process, module, require,  __dirname, __filename) {
        try {
            eval(l + '!function(code,l){' + code + '}.call(this)'); // Дополнительно прячем в именах параметров код и подключение библиотек.
        } catch (e) {
            console.log("Code execution error: " + e.message);
        }
    };
    // Вызываем sandbox без параметров, так что все имена становятся равными undefined
    sandbox.call(context); 
};

Понятно или еще комментариев дописать?

Нужно только осторожно следить за тем, какие функции и классы открывать для пользователей. Возможно, я что-то не предусмотрел, поправьте меня в таком случае.