Кроссбразуреная, кросплатформенная особенность задания (set) Node.textContent:

<a href="javascript:document.body.textContent = '<div>text</div>';">click</a>

Вставит строчку как html код:

<div>text</div>


Вступление


Для создания пароля можно использовать любые ASCII символы в диапазоне 32-126. Один из этих символов — знак меньше ("<"). Для удобства генерации пароля создал закладку (bookmark) в бразуре, в которой в url вместо ссылки на страницу используется протокол javascript.

JS генерирует пароль длиной n из набора доступных ASCII символов и вставляет в документ получившуюся строку с помощью document.body.textContent.

Url закладки
javascript:for(var a=[],i=0;i<95;i++)a.push(String.fromCharCode(32 + i));for(var b="",i=0;i<16;i++)b+=a[Math.floor(Math.random()*95)];document.body.textContent = b;

Более понятный js
document.body.textContent = createPassword();

function createPassword() {
    var PASSWORD_LENGTH = 16;

    var random = Math.random;
    var allowedChars = getAllowedCharsArr();

    for (var password = "", i = 0; i < PASSWORD_LENGTH; i++) {
        var randomIndex = Math.floor(random() * allowedChars.length);
        password += allowedChars[randomIndex]
    }

    return password;
}

function getAllowedCharsArr() {
    var FIRST_ALLOWED_ASCII_CHAR_INDEX = 32;
    var ALLOWED_CHARS_COUNT = 95;

    for (var allowedChars = [], i = 0; i < ALLOWED_CHARS_COUNT; i++) {
        var asciiCode = FIRST_ALLOWED_ASCII_CHAR_INDEX + i;
        var char = String.fromCharCode(asciiCode);
        allowedChars.push(char)
    }

    return allowedChars;
}


Обнаружение


После некоторого использования закладки заметил, что иногда пароль отображается не полностью, т.е. первые k символов из всех, заранее заданых n.

Изучение


После некоторого изучения получил следующий код:

document.body.textContent = '<div>text</div>';

Если его запустить в консоли браузера, или в script теге html, javascript отработает как надо, т.е. вставит строку вместе с символами "<" и ">".

<div>text</div>

Особенность


Но если этот же код выполнить в протоколе javascript в документе окажется html элемент:

<a href="javascript:document.body.textContent = '<div>text</div>';">click</a>

<div>text</div>

Эфимерность


Причём если сохранить ссылку на переменную:

<a href="javascript:var str='<div>text</div>';document.body.textContent = str;window.test=function(){str}">click</a>

Текст вставиться как надо:

<div>text</div>

Защита


Не используйте на своих страницах протокол javascript для вставки пользовательского неотформатированного текста через innerText/textContent

Решение


Для себя более удобным стало не вставлять текст в DOM а сразу копировать пароль в буффер обмена.

Закладка
javascript:for(var a=[],i=0;i<95;i++)a.push(String.fromCharCode(32 + i));for(var b="",i=0;i<16;i++)b+=a[Math.floor(Math.random()*95)];var c=document.createElement('input');c.style.opacity=0;document.body.appendChild(c); c.value=b;c.style.position='fixed';c.style.zIndex=1000000;c.focus();c.select(); document.execCommand('Copy');c.remove();

source
copyToClipBoard(createPassword());

function copyToClipBoard(str) {
    var input = document.createElement('textarea');
    document.body.appendChild(input);
    input.value = str;
    input.style.position = 'fixed';
    input.style.zIndex = 1000000;
    input.style.opacity = 0;
    input.focus();
    input.select();
    document.execCommand('Copy');
    input.remove();
}

function createPassword() {
    var PASSWORD_LENGTH = 16;

    var random = Math.random;
    var allowedChars = getAllowedCharsArr();

    for (var password = "", i = 0; i < PASSWORD_LENGTH; i++) {
        var randomIndex = Math.floor(random() * allowedChars.length);
        password += allowedChars[randomIndex]
    }

    return password;
}

function getAllowedCharsArr() {
    var FIRST_ALLOWED_ASCII_CHAR_INDEX = 32;
    var ALLOWED_CHARS_COUNT = 95;

    for (var allowedChars = [], i = 0; i < ALLOWED_CHARS_COUNT; i++) {
        var asciiCode = FIRST_ALLOWED_ASCII_CHAR_INDEX + i;
        var char = String.fromCharCode(asciiCode);
        allowedChars.push(char)
    }

    return allowedChars;
}


P.S.: Вместо циклов можно использовать Array.map. Кто-то считает, что так элегантнее, кто-то замечает, что это медленнее — это уже дело вкуса.

Альтернатива
javascript:var a = Array.apply(null, Array(127)).map(String.fromCharCode, String).slice(32);var b = Array.apply(null, Array(16)).map(() => a[Math.floor(Math.random() * 95)]).join("");var c = document.createElement('input');document.body.appendChild(c);c.value = b;c.style.position = 'fixed';c.style.zIndex = 1000000;c.style.opacity = 0;c.focus();c.select();document.execCommand('Copy');c.remove();

P.P.S.: Вместо закладки можно использовать extenshion: например, chrome — это также дело вкуса.

Update

Разоблачение


Из коментария mayorovp
Происходит данное не из-за работы textContent, а из-за обработки протокола javascript
Браузер выполнив js вставляет в html результат последнего выражение

javascript:'<div>text</div>'
вернёт строку, которую браузер воспримет как html документ

обойти данную особенность можно вставив в конце скрипта void (0) или любой другое выражение, не возвращающее корректного значения (строки, в некоторых браузерах числа)

Комментарии (9)


  1. mayorovp
    02.12.2015 19:27
    +13

    Это связано с тем фактом, что если после схемы javascript: указано выражение — его результат будет использован вместо html-кода.

    К примеру, если написать в адресной строке javascript:'<div>text</div>' — результат будет точно таким же. Встречая код вида javascript:document.body.textContent = '<div>text</div>', браузер (Mozilla Firefox — точно, остальные — не знаю) делает две операции:

    1. Присваивание document.body.textContent = 'div>text</div>'
    2. Замена кода документа на результат вычисленного выражения — '<div>text</div>'

    Кстати, шаг 2 является операцией перехода и отражается в истории (можно вернуться назад) — а первый шаг меняет текущую страницу.

    При удаче можно даже увидеть, как одна надпись сменяет другую.

    Причем такое «чудо» происходит при любой последовательности операторов, если последний из них является выражением. Чтобы защититься от такой «фичи», можно дописать в конец букмарклета ;void(0) — в таком случае второго шага не происходит.


    1. Nookie-Grey
      02.12.2015 19:59
      +3

      Да, верно. Ложная тревога. Спасибо за объяснение.
      Надо бы статью переписать, а может и удалить, за неимением стержня.
      Ну или оставить ради генератора пароля.
      Жёлтенько получилось.


      1. Shakirok
        02.12.2015 23:13
        +2

        Удалять не стоит. Думаю можно просто дополнить статью первым комментарием. Тогда получится довольно познавательно.


      1. denis_g
        02.12.2015 23:57

        Не стоит удалять, она в будущем может кому-нибудь очень пригодиться. А первый комментарий действительно стоит добавить в статью.


        1. Nookie-Grey
          03.12.2015 10:25

          Добавил. Спасибо хабр-сообществу за столь тёплый приём.


    1. denis_g
      03.12.2015 00:00

      А не подскажите, где можно почитать о том, почему происходит шаг 2?


      1. mayorovp
        03.12.2015 05:54
        +4

        Если бы еще я сам это знал…


        1. Vest
          03.12.2015 18:52
          +1

          Я тут нагуглил, кажется, некоторое задокументированное поведение. Не судите строго: JavaScript URIs.


      1. eyeless_watcher
        03.12.2015 09:02
        +2

        Ну это же обычное поведение браузера.
        При переходе по ссылке или вбивании чего-нибудь в адресную строку браузер просто берет контент по указанному протоколу (обычно это http(s)://, в данном случае — javascript:) и показывает в окне документа, заменяя его текущее содержимое. Разница лишь в том, как именно получается конечный контент.