FastPic Directизделие FPD-4.1 · паспорт
Паспорт изделия · Инструкция по эксплуатации

Как обойти вредоносный код в Fastpic?

Кликаете по картинке на fastpic — а открывается казино, «ваш телефон заражён» и ещё две вкладки на сдачу. Этот юзерскрипт чинит ровно одно: по ссылке на картинку открывается картинка. Сразу оригинал, без попандеров, редиректов и танцев с бубном.

Установить скрипт

Плакат в стиле советской техники безопасности: инженер в очках отгораживает монитор с фотографией от роя выскакивающих окон

Назначение изделия

Изделие «FastPic Direct» (далее — скрипт) предназначено для просмотра картинок, размещённых на fastpic, без принудительной экскурсии по рекламным сетям.

Принцип действия в трёх строках:

  1. Вы открываете ссылку вида fastpic.org/view/… или /fullview/….
  2. Скрипт останавливает страницу раньше, чем на ней успеет выполниться хоть что-нибудь, сам скачивает её HTML как текст, находит в нём спрятанный адрес оригинала и склеивает его из кусочков.
  3. Вкладка уезжает прямо на файл картинки. Конец. Занавес. Никто никуда не всплыл.

Ни одна строчка кода fastpic при этом не выполняется: текст страницы читается, а не запускается. Скрипт никуда ничего не отправляет, ничего не собирает и умеет только это — 200 строк с комментариями, можно прочитать за чашку чая.

Комплект поставки

Изделие
FastPic Direct
Версия
4.1
Размер
10 КБ
Чужого кода внутри
0 строк
Установить Скачать файл «Установить» сработает, если менеджер скриптов уже стоит. Нет — см. раздел ниже.
Показать исходник целиком (читать код перед установкой — правильно)
// ==UserScript==
// @name         FastPic Direct
// @name:ru      FastPic Direct — картинка без попандеров
// @namespace    fastpic-direct
// @version      4.1
// @description  Jump straight to the original full-size image on fastpic view/fullview pages: no popunders, no redirects.
// @description:ru Открывает оригинал картинки на fastpic сразу: без попандеров, редиректов и рекламной обвязки.
// @license      MIT
// @match        *://fastpic.org/*
// @match        *://fastpic.ru/*
// @run-at       document-start
// @grant        none
// @homepageURL  https://fastpic-direct.org/
// @downloadURL  https://fastpic-direct.org/fastpic-direct.user.js
// @updateURL    https://fastpic-direct.org/fastpic-direct.user.js
// @icon         https://fastpic-direct.org/img/icon.png
// ==/UserScript==

// How it works
// ------------
// The signed URL of the original image is in the server-rendered HTML of both
// the view and the fullview page, but (as observed 2026-09-20) no longer as
// plain text: it sits inside an inline script, chopped into string fragments
// ("ht"+"tps"+":/"+"/i1"+"28.f"+...) whose split points, like every identifier
// around them, are re-randomised on each request. That inline script only
// assigns the URL to the <img> once the site's popunder click-hijacker reports
// that it installed itself, so running the page's JavaScript to get the URL
// means accepting the popunder. The project site has the full analysis.
//
// So the script aborts the page load before any of it runs, fetches the HTML
// itself, glues the fragments back together as text (nothing from the page is
// ever executed), pulls the URL out, and sends the tab to the image file.

(function () {
    'use strict';

    // The site has 302'd /view/ to /vie%77/ (and /fullview/ to /fullvie%77/) to
    // slip out from under @match patterns and path filters. Any character can
    // be encoded and the encoding may change, so decode before testing - and
    // match the whole site above, since a match pattern cannot express this.
    let path = location.pathname;
    try { path = decodeURIComponent(path); } catch (e) { /* keep it raw */ }
    if (!/^\/(view|fullview)\//.test(path)) return;

    // Escape hatch: ?fpdirect=off loads the untouched page.
    if (/[?&]fpdirect=off/.test(location.search)) return;

    // The token is required, and not just for the server's sake: the page also
    // carries a decoy /big/ URL with no token - a hidden 1x1 background image
    // whose 404 tells the site an ad blocker ate it. Demanding md5= keeps the
    // decoy out no matter where in the document it sits.
    const BIG_RE = /https?:\/\/[a-z0-9.-]*fastpic\.(?:org|ru)\/big\/[^\s"'<>\\]*[?&]md5=[^\s"'<>\\]*/ig;

    // /view/128/2026/0824/_6851...c7c3.jpg.html -> "_6851...c7c3". The original
    // lives at i128.fastpic.org/big/2026/0824/c3/_6851...c7c3.jpg, so a genuine
    // candidate contains this name. The decoy is a different file, and this
    // keeps it out even if it grows a fake token one day.
    const NAME = (/\/([^\/.]+)\.[^\/]*$/.exec(path) || [])[1] || '';

    // How long to wait for the page HTML before giving up and saying so.
    const FETCH_TIMEOUT_MS = 15000;

    // Status texts, in Russian for Russian-language browsers.
    const T = /^ru\b/i.test(navigator.language || '') ? {
        loading: 'Достаю оригинал картинки…',
        notFound: 'Не нашёл на странице адрес оригинала. Похоже, сайт опять что-то поменял.',
        timeout: s => 'Страница не ответила за ' + s + ' с.',
        loadError: m => 'Не удалось загрузить страницу: ' + m,
        original: 'Открыть страницу как есть'
    } : {
        loading: 'Loading the original image…',
        notFound: 'Could not find the original image URL on this page.',
        timeout: s => 'The page did not answer within ' + s + ' seconds.',
        loadError: m => 'Could not load the page: ' + m,
        original: 'Open the original page'
    };

    // A rejected token 302s straight back to this page, so a bad token would
    // loop forever. Landing here seconds after we sent this tab away means the
    // image bounced; show the real page instead of trying again.
    const KEY = 'fpdirect:' + path;
    try {
        if (Date.now() - Number(sessionStorage.getItem(KEY) || 0) < 15000) {
            sessionStorage.removeItem(KEY);
            return;
        }
    } catch (e) { /* no sessionStorage; carry on without the guard */ }

    // --- Kill the page before it can run anything ---------------------------
    // window.stop() at document-start aborts the parser, so the inline ad
    // scripts further down the document are never reached.
    try { window.stop(); } catch (e) { /* nothing to stop yet */ }

    // Greasemonkey 4 injects from a background-page event rather than at true
    // document-start, so the parser may be anywhere by now: typically ~2 KB
    // into <head> (no <body> yet - a bare white page), but on a busy browser it
    // can be far enough that ad scripts are already running. window.stop() does
    // not cancel their timers, and they go on injecting into whatever document
    // is there. Timer ids are per-window and sequential, so walking down from a
    // fresh one cancels everything the page scheduled.
    try {
        for (let id = setTimeout(function () {}, 0); id >= 0; id--) {
            clearTimeout(id);
            clearInterval(id);
        }
    } catch (e) { /* best effort */ }

    // Replace whatever the parser left behind straight away. Without this the
    // tab stays blank white for as long as the fetch below takes - and forever
    // if it never settles.
    let statusEl = null;
    render(T.loading, false);

    // Belt and braces: a meta CSP stops any script that has not loaded yet.
    // Added after render(), which would otherwise throw it out with the old
    // <head>.
    try {
        const meta = document.createElement('meta');
        meta.setAttribute('http-equiv', 'Content-Security-Policy');
        meta.setAttribute('content', "script-src 'none'; frame-src 'none'; object-src 'none'");
        (document.head || document.documentElement).appendChild(meta);
    } catch (e) { /* document not ready; window.stop() already did the work */ }

    // --- Fetch the HTML ourselves and dig out the original ------------------
    // The token is signed and expires on an hour boundary one to two hours out,
    // so it has to come from a fresh copy of the page rather than being
    // reconstructed.
    const ctl = new AbortController();
    const timer = setTimeout(function () { ctl.abort(); }, FETCH_TIMEOUT_MS);
    fetch(location.href, { credentials: 'include', cache: 'no-store', signal: ctl.signal })
        .then(r => r.text())
        .then(html => {
            clearTimeout(timer);
            const url = findOriginal(html);
            if (!url) return fail(T.notFound);
            try { sessionStorage.setItem(KEY, String(Date.now())); } catch (e) { /* guard off */ }
            // &dl=1 makes the server send Content-Disposition: attachment,
            // which would download the file instead of showing it.
            location.replace(url.replace(/&amp;/g, '&').replace(/&dl=1\b/, ''));
        })
        .catch(err => {
            clearTimeout(timer);
            fail(err && err.name === 'AbortError'
                ? T.timeout(FETCH_TIMEOUT_MS / 1000)
                : T.loadError(err && err.message));
        });

    // Look in the HTML as served first, in case the site ever goes back to
    // plain text, then with the string fragments glued back together.
    function findOriginal(html) {
        return pick(html) || pick(joinFragments(html));
    }

    function pick(text) {
        const all = text.match(BIG_RE) || [];
        return all.find(u => NAME && u.indexOf(NAME) !== -1) || all[0] || null;
    }

    // Collapse every chain of adjacent string literals - "ab"+"cd"+'ef' - into
    // the single literal "abcdef". Purely textual: nothing is evaluated. The
    // obfuscator sprinkles /**/ between tokens elsewhere in the same script, so
    // comments around the plus signs are tolerated, as are simple escapes
    // inside the pieces, neither of which it uses in the URL today.
    function joinFragments(text) {
        const STR = '"(?:[^"\\\\\\n]|\\\\.)*"|\'(?:[^\'\\\\\\n]|\\\\.)*\'';
        const GAP = '(?:\\s|/\\*[^*]*\\*/)*';
        const chain = new RegExp('(?:' + STR + ')(?:' + GAP + '\\+' + GAP + '(?:' + STR + '))+', 'g');
        const piece = new RegExp(STR, 'g');
        return text.replace(chain, m => '"' + m.match(piece).map(unquote).join('') + '"');
    }

    function unquote(lit) {
        return lit.slice(1, -1)
            .replace(/\\x([0-9a-f]{2})|\\u([0-9a-f]{4})/gi,
                (_, x, u) => String.fromCharCode(parseInt(x || u, 16)))
            .replace(/\\(.)/g, '$1');
    }

    function fail(msg) {
        render(msg, true);
    }

    // window.stop() leaves whatever the parser had already built, which may be
    // a partial <head> and no <body>; replace it wholesale rather than showing
    // a torn page. Colours are set inline as well as through the stylesheet so
    // the page stays dark even if something removes the <head>.
    function render(msg, withLink) {
        try {
            if (!statusEl || !statusEl.isConnected) {
                // Injected early enough (Chromium, true document-start) there is
                // no <html> element yet, and with the parser stopped there never
                // will be - so make one.
                let root = document.documentElement;
                if (!root) {
                    root = document.createElement('html');
                    document.appendChild(root);
                }
                const head = document.createElement('head');
                const body = document.createElement('body');
                root.replaceChildren(head, body);
                const style = document.createElement('style');
                style.textContent = 'html, body { background:#111; color:#ccc; margin:0; } ' +
                    'body { font:14px/1.5 system-ui,sans-serif; display:flex; height:100vh; } ' +
                    'p { margin:auto; } a { color:#6cf; }';
                head.appendChild(style);
                root.style.background = body.style.background = '#111';
                body.style.color = '#ccc';
                statusEl = document.createElement('p');
                body.appendChild(statusEl);
            }
            statusEl.textContent = msg + ' ';
            if (withLink) {
                const a = document.createElement('a');
                a.href = location.pathname + location.search +
                    (location.search ? '&' : '?') + 'fpdirect=off';
                a.textContent = T.original;
                statusEl.appendChild(a);
            }
        } catch (e) { /* nothing to draw on; the navigation may still succeed */ }
    }
})();

Реклама — не враг. Враг — попандер

Сразу проведём границу, а то нас запишут в луддиты.

Бесплатный хостинг картинок должен на что-то жить, и баннеры — честный способ заработка. Вы видите, что это реклама, видите, где она, и сами решаете, кликать или нет. Хотите резать баннеры — это ваш выбор и ваш блокировщик; этот скрипт про другое.

Нормальная реклама

  • стоит на странице и помечена как реклама;
  • вы кликаете по ней сами и понимаете, что кликаете;
  • ваш клик по картинке остаётся кликом по картинке;
  • сайт не проверяет, послушно ли ведёт себя ваш браузер.

То, что делает fastpic

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

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

Подробный разбор с кодом: как устроен попандер и как спрятана ссылка на картинку.

Установка

Нужны две вещи: менеджер юзерскриптов (расширение браузера) и сам скрипт. Если менеджер у вас уже есть — жмите «Установить» выше и идите пить чай.

Шаг 1. Поставьте менеджер скриптов

БраузерЧто ставитьПримечания
Firefox (ПК)Violentmonkey, Tampermonkey, Greasemonkey или FireMonkeyРаботает сразу, без дополнительных галочек. Наш выбор, если вам всё равно.
ChromeTampermonkey, Violentmonkey (нужен Chrome 135+) или ScriptCatОбязателен шаг 2, иначе скрипты молча не запускаются.
EdgeTampermonkey, Violentmonkey, ScriptCatШаг 2 обязателен.
OperaTampermonkey из каталога Opera; Violentmonkey — из магазина ChromeШаг 2 обязателен.
Яндекс БраузерTampermonkey или Violentmonkey из магазина Chrome либо каталога OperaРасширения живут на browser://extensions. Шаг 2 обязателен.
Brave, VivaldiТо же, что для ChromeОфициальной документации не нашли; по идее всё как в Chrome.
Safari (macOS, iOS)Userscripts (бесплатно), Stay (бесплатно), Tampermonkey (платно)После установки включите расширение в настройках Safari и дайте ему доступ к fastpic.
AndroidFirefox + любой менеджер из первой строки; либо Edge + TampermonkeyKiwi Browser закрыт с января 2025 — не ставьте. В Cromite поддержка юзерскриптов встроена.

Список проверен по магазинам расширений 20.09.2026; мир расширений меняется быстро, так что если ссылка протухла — ищите по названию.

Шаг 2. Только для Chrome и его родни: разрешите пользовательские скрипты

С 2025 года браузеры на Chromium не дают менеджерам запускать скрипты, пока вы явно не разрешите. Это не поломка и не вирус, это Manifest V3. Лечится за полминуты:

  1. Откройте chrome://extensions (в Edge — edge://extensions, в Опере — opera://extensions, в Яндексе — browser://extensions).
  2. Найдите свой менеджер скриптов, нажмите «Сведения».
  3. Включите переключатель «Разрешить пользовательские скрипты» (Allow User Scripts).
  4. Перезагрузите вкладку с fastpic. Если не помогло — выключите и включите расширение.

В старых версиях (Chrome до 138, Edge до 144) такого переключателя нет — там вместо него включается «Режим разработчика» в правом верхнем углу той же страницы. В Опере и Яндексе, по сообщениям пользователей, иногда нужно и то и другое.

Шаг 3. Установите скрипт

Способ для ленивых (рекомендуется). Нажмите кнопку «Установить». Менеджер перехватит ссылку на .user.js, покажет исходник и спросит подтверждение. Соглашайтесь. Заодно менеджер запомнит адрес и будет сам подтягивать обновления, когда fastpic придумает новую пакость, а мы — новую версию.

В Safari с расширением Userscripts окно само не появится: откройте ссылку на скрипт, затем ткните в значок расширения — предложение установить будет там.

Способ для недоверчивых (копипаста). Нажмите «Скопировать код», затем:

МенеджерКуда вставлять
TampermonkeyЗначок расширения → «Создать новый скрипт…» → удалить заготовку, вставить код → Ctrl+S
ViolentmonkeyЗначок → шестерёнка (панель управления) → «+» → «Создать» → вставить → Ctrl+S
GreasemonkeyЗначок → «Создать пользовательский скрипт…» → вставить → Ctrl+S
ScriptCatЗначок → шестерёнка → Create New Script → вставить → Ctrl+S
Userscripts (Safari, macOS)Страница расширения → «+» → New Javascript → вставить → сохранить

Учтите: вставленный вручную скрипт сам обновляться не будет — придётся заглядывать сюда.

Проверка работоспособности

Откройте любую ссылку на картинку с fastpic. Должна открыться картинка — и больше ничего. На долю секунды может мелькнуть тёмный экран с надписью: это скрипт работает. Вот что означают его сообщения:

Что на экранеЧто это значитЧто делать
«Достаю оригинал картинки…» и сразу картинкаШтатная работаРадоваться
То же, но висит секунд 15, затем «Страница не ответила за 15 с»fastpic тормозит или недоступен; либо запрос режет другое расширениеОбновить страницу; проверить, открывается ли fastpic вообще
«Не нашёл на странице адрес оригинала»Сайт опять поменял способ прятать ссылкуПроверить, нет ли здесь новой версии скрипта. Нет — значит, мы уже в курсе и ругаемся
Обычная страница fastpic во всей красеСкрипт не запустилсяChrome и родня — см. шаг 2. Проверьте, что скрипт включён в менеджере
Белый экранСкрипт не запустился и страницу кто-то поломалТо же, что строкой выше

Аварийный люк. Если нужна именно оригинальная страница (например, скопировать BB-код для форума), допишите к адресу ?fpdirect=off — скрипт отойдёт в сторону. Ссылка «Открыть страницу как есть» на экране ошибки делает то же самое. Попандер в этом режиме, разумеется, снова в строю: кликайте осмотрительно.

Гарантийные обязательства

Изготовитель гарантирует соответствие изделия описанию при соблюдении потребителем правил эксплуатации, то есть примерно ничего не гарантирует: лицензия MIT, «как есть».

Чего скрипт не делает, чтобы не было обид:

  • не блокирует рекламу — ни на fastpic, ни где-либо ещё; он просто не даёт странице просмотра загрузиться;
  • не трогает ничего, кроме страниц /view/ и /fullview/ на fastpic.org и fastpic.ru: главная, загрузка картинок и всё остальное работают как обычно;
  • не обходит никаких паролей и ограничений доступа: адрес оригинала сайт сам присылает в HTML каждому посетителю — просто мелко нарезанным;
  • не вечен. Судя по комментариям в коде самого fastpic, там активно воюют с блокировщиками, так что способ прятать ссылку ещё поменяется. История правок — на странице «История и ЧаВо».

Проверено: Firefox (в том числе вживую, с Greasemonkey) и Chromium — на движках, указанных в штампе ОТК. Остальные сочетания браузеров и менеджеров собраны по документации; если у вас что-то не взлетело — это ценная информация.