Pull to refresh

Как я учился защищать изображения

Reading time 5 min
Views 78K

Изображение защиты

В этой статье хочу изложить нелёгкий путь, который я прошёл «защищая» изображения в вебе. Перед тем, как мы начнём это увлекательное путешествие, хочу обозначить два подхода в деле защиты изображений:
  1. ограничение/запрет постинга прямых ссылок на оригиналы изображений
  2. вы параноик и пытаетесь ограничить распростронение копий изображений

UPDATE
Универсальной защиты конечно же не существует. Статья о том, как не подставлять напрямую данные из GET в SQL-запросы. Только в контексте защиты изображений.


Ограничиваем копии: мой детский велосипед


Вначале моего пути традиционно был велосипед. Много лет тому назад я разрабатывал один замечательный проект. Там было очень много чудесных фотографий животных и природы. Именно эти фотографии (а точнее их полноразмерный вариант) надо было защищать со всей силой. Клиент хотел не просто запретить прямые ссылки на файлы изображений, а лишить пользователя возможнсти скачать эти самые изображения. При этом накладывать водяные знаки не желал.

Мы уже читали о том, что программисты всё время врут. Поэтому пришлось делать то, чего хотел клиент. Решение оказалось вполне даже симпатичным. При запросе страницы с фотографией, мы генерируем некий $secretKey и сохраняем в сессию под этим ключом путь к полноразмерной копии изоражения:

public function actionView()
{
    // ...
    $_SESSION['protected-photos'][$secretKey]['file'] = $photoPath;
    // ...
}

Во вьюшке же указываем путь к фотографии в следующем виде:

<img src="/photo/source/{secretKey}" />

Теперь в actionSource мы получаем из сессии путь к полноразмерной копии фото, отправляем её с правильными заголовками и очищаем путь к полноразмерному файлу:

public function actionSource()
{
    $secretKey= $_GET['key'];
    $session = &$_SESSION['protected-photos'];
    $file = $session[$secretKey]['file'];
    if (is_file($file)) {
        header('Content-type: image/jpeg');
        echo file_get_contents($file);
    }
    $session[$secretKey]['file'] = '';
}

В итоге если пользователь попытается скачать / открыть в новой вкладке / расшарить картинку, ему вернётся её маленькая копия.

Важно: Слабое место такого подхода довольно очевидно: если страницу с фотографией запросить не из браузера, а скажем через wget. В этом случае тег img не сделает запрос /photo/source/{secretKey}. Таким образом он будет содержать полноразмерную копию фотографии.

Ограничиваем прямые ссылки: .htaccess


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

RewriteEngine On
RewriteCond %{HTTP_REFERER} !^http://(.+\.)?mysite\.com/ [NC]
RewriteCond %{HTTP_REFERER} !^$
RewriteRule .*\.(jpe?g|gif|png)$ http://i.imgur.com/qX4w7.gif [L]

Первая строка содержит директиву, которая включает работу механизма преобразований. Здесь всё просто. Второй строкой мы блокируем любые сайты, кроме нашего собственного mysite.com. Код [NC] означает «без вариантов», иными словами регистронезависимое соответствие URL. Третьей строкой мы разрешаем пустые рефералы. И, наконец, последняя строка мачит все файлы с расширением JPEG, JPG, GIF или PNG и заменяет их изображением qX4w7.gif с сервера imgur.com.

При необходимости можно поступть иначе: запретить прямые ссылки на изображения для конкретных доменов.

RewriteEngine On
RewriteCond %{HTTP_REFERER} ^http://(.+\.)?myspace\.com/ [NC,OR]
RewriteCond %{HTTP_REFERER} ^http://(.+\.)?blogspot\.com/ [NC,OR]
RewriteCond %{HTTP_REFERER} ^http://(.+\.)?livejournal\.com/ [NC]
RewriteRule .*\.(jpe?g|gif|png)$ http://i.imgur.com/qX4w7.gif [L]

Каждый RewriteCond, кроме последнего, должен содержать код [NC, OR]. OR означает «или следующий», т.е. совпадение с текущим доменом или следующим.

Также вместо изображения-заглушки можно вернуть HTTP ошибку с кодом 403:

RewriteRule .*\.(jpe?g|gif|png)$ - [F]

Важно: не пытайтесь вернуть вместо изображений HTML страницу. Вы можете вернуть либо другое изображение, либоHTTP-ошибку.

Ограничиваем прямые ссылки: nginx


Для nginx всё аналогично:

location ~* \.(jpe?g|gif|png)$ {
        set $bad_ref "N";
        if ($http_referer !~ ^(http://(.+\.)?myspace\.com|http://(.+\.)?blogspot\.com|http://(.+\.)?livejournal\.com)) {
           set $bad_ref "Y";
        }
        if ($bad_ref = "Y") {
           return 444;
        }
}

Update: VBart подсказал в своём комментарии, что намного лучше для этих целей использовать ngx_http_referer_module.

Ограничиваем прямые ссылки: Amazon CloudFront Signed URLs


Amazon CloudFront является одним из лучших вариантов доставки контента пользователям. Помимо своих прямых обязанностей рядового CDN'а, он также даёт возможность генерировать подписанные ссылки. Такие ссылки дают возможность ограничить доступ к файлу по временному диапазону, а также по IP. Таким образом, например, можно указать, что изображение будет доступно в течение 10 минут. Или 7 дней начиная с завтрашнего.

Всреднем, ссылка на файл имеет следующий вид:

1d111111abcdef8.cloudfront.net/image.jpg?2color=red&size=medium3&Policy=eyANCiAgICEXAMPLEW1lbnQiOiBbeyANCiAgICAgICJSZXNvdXJjZSI6Imh0dHA 6Ly9kemJlc3FtN3VuMW0wLmNsb3VkZnJvbnQubmV0L2RlbW8ucGhwIiwgDQogICAgICAiQ 29uZGl0aW9uIjp7IA0KICAgICAgICAgIklwQWRkcmVzcyI6eyJBV1M6U291cmNlSXAiOiI yMDcuMTcxLjE4MC4xMDEvMzIifSwNCiAgICAgICAgICJEYXRlR3JlYXRlclRoYW4iOnsiQ VdTOkVwb2NoVGltZSI6MTI5Njg2MDE3Nn0sDQogICAgICAgICAiRGF0ZUxlc3NUaGFuIjp 7IkFXUzpFcG9jaFRpbWUiOjEyOTY4NjAyMjZ9DQogICAgICB9IA0KICAgfV0gDQp9DQo4&Signature=nitfHRCrtziwO2HwPfWw~yYDhUF5EwRunQA-j19DzZrvDh6hQ73lDx~ -ar3UocvvRQVw6EkC~GdpGQyyOSKQim-TxAnW7d8F5Kkai9HVx0FIu-5jcQb0UEmat EXAMPLE3ReXySpLSMj0yCd3ZAB4UcBCAqEijkytL6f3fVYNGQI65&Key-Pair-Id=APKA9ONS7QCOWEXAMPLE

А теперь по пунктам:
  1. Базовая ссылка на ваше изображение. Это ссылка, которую вы использовали для доступа к изображению и ранее, до подписанных ссылок.
  2. Произвольные параметры запроса, которые обычно используются для логирования доступа к изображениям. CloudFront позволяет передавать, кэшировать и логировать эти параметры. Важно: имя параметров не должно совпадать с зарезервированными самим CloudFront: Expires, Key-Pair-Id, Policy, Signature. Лучше всего добавлять к вашим параметрам префикс x-. Это будет особенно полезно, если ваши изображения хранятся на Amazon S3.
  3. Правила доступа к изображениб в JSON-формате и без пробелов (детали).
  4. Хэшированная и подписанная версия правил доступа из предыдущего пункта (детали).
  5. Ключ подписи (детали).

Важно: CloudFront не поддерживает CNAMEs с HTTPS. Т.е. вы не сможете заменить d111111abcdef8.cloudfront.net на images.example.com. Есть два варианта решений проблемы:
  1. Вернуть использование домена https://*.cloudfront.com для изображений.
  2. Оставить домен images.example.com, но использовать его через протокол HTTP.
Выбор одного из двух вариантов по сути это дело вкуса. Принципиально они между собой не отличаются.

Эпилог


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

  1. Hotlinking: Генератор .htaccess
  2. Hotlinking: Конфигурация .htaccess
  3. Hotlinking: Пример настройки nginx
  4. Hotlinking: Проверка
  5. Amazon CloudFront Signed URLs
Tags:
Hubs:
+30
Comments 121
Comments Comments 121

Articles