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

Назначение изделия
Изделие «FastPic Direct» (далее — скрипт) предназначено для просмотра картинок, размещённых на fastpic, без принудительной экскурсии по рекламным сетям.
Принцип действия в трёх строках:
- Вы открываете ссылку вида
fastpic.org/view/…или/fullview/…. - Скрипт останавливает страницу раньше, чем на ней успеет выполниться хоть что-нибудь, сам скачивает её HTML как текст, находит в нём спрятанный адрес оригинала и склеивает его из кусочков.
- Вкладка уезжает прямо на файл картинки. Конец. Занавес. Никто никуда не всплыл.
Ни одна строчка кода 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(/&/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 | Работает сразу, без дополнительных галочек. Наш выбор, если вам всё равно. |
| Chrome | Tampermonkey, Violentmonkey (нужен Chrome 135+) или ScriptCat | Обязателен шаг 2, иначе скрипты молча не запускаются. |
| Edge | Tampermonkey, Violentmonkey, ScriptCat | Шаг 2 обязателен. |
| Opera | Tampermonkey из каталога Opera; Violentmonkey — из магазина Chrome | Шаг 2 обязателен. |
| Яндекс Браузер | Tampermonkey или Violentmonkey из магазина Chrome либо каталога Opera | Расширения живут на browser://extensions. Шаг 2 обязателен. |
| Brave, Vivaldi | То же, что для Chrome | Официальной документации не нашли; по идее всё как в Chrome. |
| Safari (macOS, iOS) | Userscripts (бесплатно), Stay (бесплатно), Tampermonkey (платно) | После установки включите расширение в настройках Safari и дайте ему доступ к fastpic. |
| Android | Firefox + любой менеджер из первой строки; либо Edge + Tampermonkey | Kiwi Browser закрыт с января 2025 — не ставьте. В Cromite поддержка юзерскриптов встроена. |
Список проверен по магазинам расширений 20.09.2026; мир расширений меняется быстро, так что если ссылка протухла — ищите по названию.
Шаг 2. Только для Chrome и его родни: разрешите пользовательские скрипты
С 2025 года браузеры на Chromium не дают менеджерам запускать скрипты, пока вы явно не разрешите. Это не поломка и не вирус, это Manifest V3. Лечится за полминуты:
- Откройте
chrome://extensions(в Edge —edge://extensions, в Опере —opera://extensions, в Яндексе —browser://extensions). - Найдите свой менеджер скриптов, нажмите «Сведения».
- Включите переключатель «Разрешить пользовательские скрипты» (Allow User Scripts).
- Перезагрузите вкладку с 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 — на движках, указанных в штампе ОТК. Остальные сочетания браузеров и менеджеров собраны по документации; если у вас что-то не взлетело — это ценная информация.