Как предотвратить атаки с использованием скриптов - PullRequest
5 голосов
/ 29 октября 2019

Intro

Эта тема была бичом многих вопросов и ответов на StackOverflow - и на многих других технических форумах;однако большинство из них относятся к конкретным условиям и, что еще хуже, «общая защита» в предотвращении внедрения скриптов с помощью dev-tools-console, или dev-tools-elements, или даже address-bar считается «невозможной» для защиты. Этот вопрос предназначен для решения этих проблем и служит текущим и историческим справочным материалом по мере совершенствования технологии - или обнаруживаются новые / более совершенные методы для решения проблем безопасности браузера - особенно связанных с script-injection атаками.

Проблемы

Есть много способов извлечь или манипулировать информацией «на лету»;в частности, очень легко перехватить информацию, собранную с ввода, для передачи на сервер - независимо от SSL / TLS.

пример перехвата

Посмотрите здесь Независимо от того, насколько "сырой" она являетсяможно легко использовать принцип для изготовления шаблона, просто копируя + вставляя в eval() в консоли браузера, чтобы делать все виды неприятных вещей, таких как:

  • console.log() перехваченная информацияв пути через XHR
  • манипулировать POST -данными, изменяя пользовательские ссылки, такие как UUIDs
  • , кормить альтернативу целевого сервераGET (& post) запрашивает информацию для ретрансляции (или получения) информации путем проверки JS-кода, cookies и headers

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

Мы все знаем "васне могу доверять клиенту " ивсегда должен нести ответственность за безопасность;однако - как насчет конфиденциальности / безопасности наших любимых посетителей? Многие люди создают «какое-то быстрое приложение» на JavaScript и либо не знают (или не заботятся) о внутренней безопасности.

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

Усилия

И Google, и Facebook реализовали некоторые способы смягчения этих проблем, и они работают;таким образом, это НЕ «невозможно», однако, они очень специфичны для соответствующих платформ, и для их реализации требуется использование целых фреймворков, а также много работы - только для охвата основ.

Независимо от того, насколько «уродливым»«могут появиться некоторые из этих защитных механизмов;цель состоит в том, чтобы помочь (смягчить / предотвратить) проблемы безопасности до некоторой степени, что усложнит злоумышленнику. К настоящему времени все знают: «вы не можете удержать хакера, вы можете только препятствовать его усилиям» .

Инструменты и требования

Цель состоит в том, чтобы иметь простойнабор инструментов (функций):

  • они ДОЛЖНЫ быть написаны простым (ванильным) javascript
  • вместе, они НЕ должны превышать несколько строк кода (максимум 200)
  • они должны быть immutable, предотвращая «повторный захват» злоумышленником
  • они НЕ ДОЛЖНЫ конфликтовать с какими-либо (популярными) JS-средами, такими как React, Angular и т. Д.
  • НЕ обязательно должен быть «красивым», но, по крайней мере, читабельным, «однострочное» приветствуется
  • кросс-браузерно совместимо, по крайней мере, с хорошим процентилем

1 Ответ

2 голосов
/ 29 октября 2019

Runtime Reflection / Introspection

Это способ решения некоторых из этих проблем, и я не утверждаю, что это «лучший» способ (вообще), это попытка. Если бы кто-то мог перехватить некоторые «эксплуатируемые» функции и методы и посмотреть, был ли сделан «вызов» (за вызов) с сервера, который его породил, или нет, то это могло бы оказаться полезным, так как мы можем видеть, пришел ли вызов «изthin air "(dev-tools).

Если этот подход будет принят, то сначала нам понадобится функция, которая захватывает call-stack и отбрасывает то, что не является FUBU (длянас от нас). Если результат этой функции пустой, хазаа! - мы не сделали вызов, и мы можем продолжить соответственно.

слово или два

Чтобы сделать это максимально коротким и простым, следующие примеры кода следуют DRYKIS принципы, которые:

  • не повторяйте себя, не усложняйте
  • "меньше кода" приветствует адепта
  • «слишком много кода и комментариев» отпугивают всех
  • , если вы можете читать код - продолжайте и сделайте его красивым

Сказав это, простите мою "короткую руку",объяснение будет следовать

сначала нам понадобятся некоторые константы и наш стек-получатель

    const MAIN = window;
    const VOID = (function(){}()); // paranoid
    const HOST = `https://${location.host}`; // if not `https` then ... ?

    const stak = function(x,a, e,s,r,h,o)
    {
        a=(a||''); e=(new Error('.')); s=e.stack.split('\n'); s.shift();  r=[]; h=HOSTPURL; o=['_fake_']; s.forEach((i)=>
        {
            if(i.indexOf(h)<0){return}; let p,c,f,l,q; q=1; p=i.trim().split(h); c=p[0].split('@').join('').split('at ').join('').trim();
            c=c.split(' ')[0];if(!c){c='anon'}; o.forEach((y)=>{if(((c.indexOf(y)==0)||(c.indexOf('.'+y)>0))&&(a.indexOf(y)<0)){q=0}}); if(!q){return};
            p=p[1].split(' '); f=p[0]; if(f.indexOf(':')>0){p=f.split(':'); f=p[0]}else{p=p.pop().split(':')}; if(f=='/'){return};
            l=p[1]; r[r.length]=([c,f,l]).join(' ');
        });
        if(!isNaN(x*1)){return r[x]}; return r;
    };

После сжимания, не забывая, это было написано "на лету" как "доказательство концепции", пока проверено и работает. Отредактируйте по своему желанию.

stak() - краткое объяснение
  • только 2 релевантных аргумента - это первые 2, остальные - потому что ... лень (короткий ответ)
  • оба аргумента являются необязательными
  • если 1-й аргумент x является числом, то, например, stack(0) возвращает 1-й элемент в журнале или undefined
  • , если2-й аргумент a является либо string -или array, то, например, stack(undefined, "anonymous") допускает «анонимный», даже если он был «опущен» в o
  • остальная часть кода просто быстро анализирует стек, это должно работать как в браузерах, основанных на webkit, так и в gecko (chrome & firefox)
  • В результате получается массив строк, каждая строка представляет собой запись журнала, разделеннуюодин пробел как function file line
  • , если имя домена не найдено в записи журнала (часть имени файла перед синтаксическим анализом), тогда оно не будет в результате
  • по умолчаниюон игнорирует имя файла / (точно), поэтому, если вы протестируете этот код, помещение в отдельный файл .js даст лучшие результаты, чем вindex.html (обычно) - или какой бы механизм веб-корня ни использовался
  • пока не беспокойтесь о _fake_, теперь он находится в функции jack ниже

сейчаснам нужны некоторые инструменты

bore() - получить / установить / скопировать некоторое значение объекта по строковой ссылке
const bore = function(o,k,v)
{
    if(((typeof k)!='string')||(k.trim().length<1)){return}; // invalid
    if(v===VOID){return (new Function("a",`return a.${k}`))(o)}; // get
    if(v===null){(new Function("a",`delete a.${k}`))(o); return true}; // rip
    (new Function("a","z",`a.${k}=z`))(o,v); return true; // set
};
bake() - сокращение для усиления существующих свойств объекта (или определения новых)
const bake = function(o,k,v)
{
    if(!o||!o.hasOwnProperty){return}; if(v==VOID){v=o[k]};
    let c={enumerable:false,configurable:false,writable:false,value:v};
    let r=true; try{Object.defineProperty(o,k,c);}catch(e){r=false};
    return r;
};

bake& bore - rundown

Это неверное объяснение, поэтому некоторых быстрых примеров должно хватить

  • , используя bore to get свойство: console.log(bore(window,"XMLHttpRequest.prototype.open"))
  • с использованием bore до установить свойство: bore(window,"XMLHttpRequest.prototype.open",function(){return "foo"})
  • с использованием bore до rip (уничтожить небрежно): bore(window,"XMLHttpRequest.prototype.open",null)
  • используя bake до укрепить существующее свойство: bake(XMLHttpRequest.prototype,'open')
  • используя bake до определить новое (сложное) свойство: bake(XMLHttpRequest.prototype,'bark',function(){return "woof!"})

перехватывающие функции и конструкции

Теперь мы можем использовать все вышеперечисленное в наших интересах, поскольку мы разрабатываем простой, но эффективный перехватчик, отнюдь не "perfect ", но этого должно быть достаточно;пояснение следующее:

const jack = function(k,v)
{
    if(((typeof k)!='string')||!k.trim()){return}; // invalid reference
    if(!!v&&((typeof v)!='function')){return}; // invalid callback func
    if(!v){return this[k]}; // return existing definition, or undefined
    if(k in this){this[k].list[(this[k].list.length)]=v; return}; //add
    let h,n; h=k.split('.'); n=h.pop(); h=h.join('.'); // name & holder
    this[k]={func:bore(MAIN,k),list:[v]}; // define new callback object

    bore(MAIN,k,null); let f={[`_fake_${k}`]:function()
    {
        let r,j,a,z,q; j='_fake_'; r=stak(0,j); r=(r||'').split(' ')[0];
        if(!r.startsWith(j)&&(r.indexOf(`.${j}`)<0)){fail(`:(`);return};
        r=jack((r.split(j).pop())); a=([].slice.call(arguments));
        for(let p in r.list)
        {
            if(!r.list.hasOwnProperty(p)||q){continue}; let i,x;
            i=r.list[p].toString(); x=(new Function("y",`return {[y]:${i}}[y];`))(j);
            q=x.apply(r,a); if(q==VOID){return}; if(!Array.isArray(q)){q=[q]};
            z=r.func.apply(this,q);
        };
        return z;
    }}[`_fake_${k}`];

    bake(f,'name',`_fake_${k}`); bake((h?bore(MAIN,h):MAIN),n,f);
    try{bore(MAIN,k).prototype=Object.create(this[k].func.prototype)}
    catch(e){};
}.bind({});
jack() - объяснение
  • принимает 2 аргумента: первый - как строка (используется для bore), второй - как перехватчик (функция)
  • первые несколько комментариев объясняют немного ... строка «добавить» просто добавляет другой перехватчик к той же ссылке
  • jack удаляет существующую функцию, убирает ее, затем использует «функции-перехватчики» дляаргументы воспроизведения
  • перехватчики могут либо вернуть undefined, либо значение, если ни одно из значений не возвращается, исходная функция не вызывается
  • первое значение, возвращаемое перехватчиком, используется как аргумент (ы) для вызова оригинала, а возвращение является результатом для вызывающего / вызывающего
  • , что fail(":(") является преднамеренным;Ошибка будет выдана, если у вас нет этой функции - только если jack() не удалось.

Примеры

Давайте запретим использование eval в консоли -или адресная строка

jack("eval",function(a){if(stak(0)){return a}; alert("having fun?")});

расширяемость

Если вы хотите DRY-er способ взаимодействия с jack, следующее проверено и хорошо работает:

const hijack = function(l,f)
{
    if(Array.isArray(l)){l.forEach((i)=>{jack(i,f)});return};
};

Теперь вы можете перехватывать массово, например так:

hijack(['eval','XMLHttpRequest.prototype.open'],function()
{if(stak(0)){return ([].slice.call(arguments))}; alert("gotcha!")});

Умный атакующий может затем использовать Elements (dev-инструмент), чтобы изменить атрибут некоторого элемента, передав ему событие onclick, тогда наш перехватчик не поймает этого;однако, мы можем использовать наблюдатель мутации и с этим шпионом «изменения атрибутов». После изменения атрибута (или нового узла) мы можем проверить, были ли внесены изменения FUBU (или нет) с помощью нашей stak() проверки:

const watchDog=(new MutationObserver(function(l)
{
   if(!stak(0)){alert("you again! :D");return};
}));

watchDog.observe(document.documentElement,{childList:true,subtree:true,attributes:true});

Заключение

Это было всего лишь несколько способов справиться с плохой проблемой;хотя я надеюсь, что кто-то найдет это полезным, и, пожалуйста, не стесняйтесь редактировать этот ответ или публиковать более (или альтернативные / лучшие) способы повышения безопасности интерфейса.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...