Почему rel='noopener' устарел для ссылок
Почему rel=noopener устарел для ссылок target=_blank, что такое reverse tabnabbing и когда по-прежнему нужны noreferrer, opener или COOP.
Для обычной ссылки с target="_blank" атрибут rel="noopener" теперь избыточен: все актуальные версии Chrome, Edge, Firefox и Safari применяют поведение noopener автоматически, поэтому «голый» target="_blank" уже устанавливает window.opener в null.
Если вы всё ещё пишете его по мышечной памяти или наблюдаете, как линтер подсвечивает единственный забытый якорь, вы латаете дыру, которую браузеры закрыли много лет назад. В этой статье разбирается, какую уязвимость этот атрибут должен был предотвращать, когда браузеры сделали исправление поведением по умолчанию и в каких именно случаях rel всё ещё выполняет реальную работу: noreferrer (не применяется автоматически), rel="opener" (возврат прежнего поведения) и заголовок Cross-Origin-Opener-Policy для контроля на уровне всего сайта.
Ключевые выводы
- В современных браузерах «голый»
target="_blank"уже обнуляетwindow.opener, поэтому ручное добавлениеrel="noopener"— это эшелонированная защита от дыры, которую браузер уже закрыл. - Неявный
noopenerвнедрялся поэтапно (Safari в 2018–19, Firefox 79 в середине 2020, Chromium 88 в начале 2021) и теперь является частью стандарта WHATWG HTML. - По данным caniuse.com, неявный
noopenerпокрывает примерно 95% мирового использования браузеров. noreferrerне применяется неявно: он по-прежнему удаляет заголовокReferer, а также подразумеваетnoopener, поэтому добавляйте его только тогда, когда вам нужна приватность реферера.- Используйте
rel="opener", чтобы вернуть доступ кwindow.opener, иCross-Origin-Opener-Policy: same-origin, чтобы разорвать связь с opener для всего документа в одном месте.
Исходная проблема: reverse tabnabbing
До того как браузеры изменили поведение по умолчанию, ссылка с target="_blank" передавала вновь открытой странице живую ссылку обратно на страницу, которая её открыла. Reverse tabnabbing — это атака, эксплуатирующая такое поведение: страница назначения читает window.opener и перенаправляет исходную вкладку на фишинговую копию, пока пользователь сосредоточен на новой вкладке. Каноническое объяснение проблемы от Матиаса Байненса формулирует это прямо: везде, где существует window.opener, открытая страница может увести открывшую её страницу куда угодно — независимо от того, к какому origin относится каждая из них.
Эксплойт — это одна строка кода, выполняющаяся в открытом документе:
if (window.opener) {
window.opener.location = 'https://you-re-hacked.com';
}
Критически важная деталь: это работает между разными origin. Чтение и запись window.opener.location не блокируются, когда страницы приходят с разных хостов, поэтому ни политика одного источника (same-origin policy), ни CORS не мешают перенаправить открывшую страницу. Это делало атаку опасной везде, где вы отображаете пользовательские или сторонние ссылки (форумы, комментарии, поля профиля), то есть там, где злоумышленник контролирует href.
Что изменилось: target=“_blank” теперь подразумевает rel=“noopener”
Discover how at OpenReplay.com.
Браузеры исправили поведение по умолчанию. Для элементов <a>, <area> и <form> атрибут target="_blank" теперь даёт тот же эффект, что и явно прописанный rel="noopener": открытый документ получает null из window.opener, и никакого атрибута для этого не требуется. Это поведение закреплено в спецификации WHATWG HTML, чьи правила перехода по гиперссылке трактуют любой target _blank как noopener, если только ссылка явно не отказывается от этого с помощью rel="opener". OWASP теперь отсылает читателей к тому же стандартизированному поведению по умолчанию и считает атаку в основном закрытой в вечнозелёных браузерах.
Изменение выкатывалось примерно три года, так что «современные браузеры так делают» — это временная шкала, а не одна конкретная дата:
| Движок | Первая стабильная версия с неявным noopener | Примерная дата выпуска |
|---|---|---|
| Safari / WebKit | Safari 12.1 (превью в Tech Preview 68) | Конец 2018 – 2019 |
| Firefox / Gecko | Firefox 79 | Середина 2020 |
| Chromium (Chrome, Edge) | Chrome/Edge 88 | Начало 2021 |
Обратите внимание: эти версии описывают именно неявное поведение, а не момент, когда появилась поддержка самого атрибута rel="noopener". Она появилась годами ранее и является другой вехой. Таблица caniuse по неявному noopener оценивает глобальную поддержку примерно в 95%, при этом вечнозелёные браузеры покрыты примерно с 2018 года. Оставшаяся доля невелика, но реальна, поэтому сверьтесь с собственной аналитикой, прежде чем вырезать атрибут. Заметное исключение — устаревший Edge на не-Chromium движке.
«Устарело писать вручную» не равно «бесполезно»
То, что rel="noopener" избыточно печатать, не делает бессмысленным весь атрибут rel. Автоматическим стало именно ключевое слово noopener. Остальные по-прежнему меняют поведение:
| Ключевое слово | Что делает | Нужно ли писать вручную в 2026-м? |
|---|---|---|
noopener | Обнуляет window.opener на открытой странице | Нет, применяется неявно при target="_blank" |
noreferrer | Удаляет заголовок Referer и подразумевает noopener | Только когда нужна приватность реферера |
opener | Восстанавливает window.opener (возврат прежнего поведения) | Да, когда вам действительно нужна эта ссылка |
noreferrer не применяется неявно. Он по-прежнему подавляет заголовок Referer, поэтому добавляйте его только тогда, когда вы действительно хотите скрыть исходный URL от страницы назначения. Он также бесплатно даёт преимущество в безопасности: поскольку noreferrer обнуляет и opener, добавление рядом noopener ничего не даёт. Таким образом, распространённая связка rel="noopener noreferrer" в современных браузерах избыточна вдвойне, ведь noreferrer сам по себе покрывает обе задачи.
Если вам действительно нужно, чтобы открытая страница сохранила ссылку window.opener (скажем, попап, который отправляет сообщение обратно), явно включите это через rel="opener". Заметки о выпуске WebKit, представившие изменение, описывают это так же: безопасное поведение теперь является поведением по умолчанию, а rel="opener" — это способ намеренно его обратить.
Одна честная оговорка о поддержке легаси: добавление rel="noopener" всё равно безвредно. Документация по аудиту Lighthouse от Chrome отмечает, что явное указание атрибута всё ещё даёт некоторую защиту тем, кто застрял на старом движке вроде Edge Legacy. В современных браузерах это шум, но это не ошибка.
COOP: масштабируемый контроль на уровне всего сайта
Чтобы разорвать общий доступ к window.opener для целого документа в одном месте, отправляйте заголовок ответа Cross-Origin-Opener-Policy: same-origin вместо того, чтобы украшать атрибутами каждую ссылку. Заголовок Cross-Origin-Opener-Policy (COOP) определяет, попадёт ли вновь открытый документ верхнего уровня в вашу группу контекстов просмотра (browsing context group) или получит собственную. При значении same-origin кросс-доменные документы оказываются в отдельной группе, и ссылки между ними и открывшей страницей разрываются — это закрывает канал opener один раз и централизованно, а не для каждого якоря по отдельности.
# nginx
add_header Cross-Origin-Opener-Policy "same-origin";
// Express
app.use((req, res, next) => {
res.set('Cross-Origin-Opener-Policy', 'same-origin');
next();
});
Одно ограничение: COOP передаётся только как HTTP-заголовок ответа. Эквивалента через <meta http-equiv> не существует, поэтому если ваша инфраструктура не позволяет устанавливать заголовки ответа, применить COOP не получится. Он широко поддерживается в актуальных браузерах, и его стоит включить как эшелонированную защиту. Session replay реального пользователя, кликающего по внешней ссылке с target="_blank", — практичный способ убедиться, что исходная вкладка никуда не переходила, а также воспроизвести любое сообщение о неожиданной навигации ровно в том браузере, которым пользовался человек.
Вердикт: что делать сейчас
Для нового кода, ориентированного на современные браузеры, не добавляйте rel="noopener" вручную — браузер сделает это за вас. Вы можете спокойно ослабить правила линтера, требующие его для каждого target="_blank", например react/jsx-no-target-blank; оставляйте правило включённым, только если вам необходимо покрыть Edge Legacy или другие движки до 2021 года. Добавляйте rel="noreferrer" тогда и только тогда, когда хотите подавить заголовок Referer. Используйте rel="opener" в редких случаях, когда вам нужна обратная ссылка на открывшую страницу. Для масштабируемой гарантии на уровне всего документа отправляйте Cross-Origin-Opener-Policy: same-origin.
Стоит отметить: более ранние рекомендации, включая собственный старый пост OpenReplay, советующий rel="noreferrer noopener" для каждой ссылки, трактуют noopener как нечто, что нужно писать всегда, и не учитывают неявное поведение по умолчанию. Это отражает привычку, за которую индустрия держалась ещё долго после того, как браузеры двинулись дальше. Точная позиция на 2026 год уже, чем прежняя: noopener теперь применяется по умолчанию, поэтому добавлять его вручную устарело, тогда как noreferrer, rel="opener" и COOP по-прежнему решают каждый свою отдельную задачу. Обращайтесь к ним осознанно, а остальное оставьте браузеру.
Часто задаваемые вопросы
Включает ли rel='noreferrer' в себя rel='noopener'?
Да. Указание rel='noreferrer' автоматически подразумевает rel='noopener', поэтому оно обнуляет window.opener в дополнение к удалению заголовка Referer. Это означает, что распространённая связка rel='noopener noreferrer' в современных браузерах избыточна вдвойне: один только noreferrer покрывает и обнуление opener, и подавление реферера. Добавляйте noreferrer только тогда, когда вы действительно хотите скрыть исходный URL от страницы назначения.
Что произойдёт, если я сегодня полностью опущу rel='noopener' у ссылки с target='_blank'?
В современных браузерах — ничего опасного. «Голый» target='_blank' уже устанавливает window.opener в null, поскольку Chrome, Edge, Firefox и Safari применяют поведение noopener неявно — правило, закреплённое в стандарте WHATWG HTML. Caniuse оценивает это примерно в 95% мирового использования браузеров. Reverse tabnabbing по умолчанию не работает; единственный пробел — устаревшие движки вроде не-Chromium Edge Legacy.
Можно ли задать Cross-Origin-Opener-Policy через meta-тег вместо заголовка?
Нет. COOP передаётся только как HTTP-заголовок ответа, и эквивалента через meta http-equiv не существует. Если ваша инфраструктура не позволяет устанавливать заголовки ответа, применить COOP не получится и придётся полагаться на атрибуты rel для отдельных ссылок или на неявное поведение noopener по умолчанию. Когда заголовки задать можно, отправка Cross-Origin-Opener-Policy: same-origin разрывает общий доступ к window.opener для всего документа в одном центральном месте, а не украшает атрибутами каждый якорь.
Как вернуть доступ к window.opener, когда он действительно нужен?
Явно используйте rel='opener' на ссылке. Поскольку безопасное поведение с обнулением window.opener теперь является умолчанием браузера, rel='opener' — это способ намеренно восстановить ссылку на открывшую страницу, например когда попапу нужно отправить сообщение обратно странице, которая его открыла. Это обращает неявное поведение noopener для конкретной ссылки, не затрагивая остальные.