Вы знаете, чем отличается %{REQUEST_URI} в Apache mod_rewrite от $_SERVER["REQUEST_URI"] в PHP?
Сможете в .htaccess на уровне Apache сделать корректную переадресацию 301 с домена с префиксом www или на него?
Для последнего вопроса я и сейчас не смогу предложить решение. Причина в протоколе HTTP/1.1, который пришлось изучить подробнее, когда «изобретал велосипед» (создавал ядро для сайта).
Всё дело в HTTP-заголовке запроса «Host:». При определённых условиях там может быть всё, что угодно, причём сервер должен полностью это проигнорировать согласно HTTP/1.1. Большинство же разработчиков используют значение этого поля, например, для SEO-оптимизаций. Забегая вперёд, скажу, что дополнительный прокси (например, nginx) позволит решить эту проблему.
Для иллюстрации некорректного поведения серверов решил перебрать сайты компаний Хабра. Для дюжины сайтов сделал это вручную, а потом обнаружил, что некоторые сайты на ошибочные запросы отвечают «правильно». После этого была написана небольшая утилита для тестирования, что позволило увеличить количество тестовых шаблонов и проверяемых сайтов.
Что же скрывает REQUEST_URI в HTTP/1.1?
Начну с протокола HTTP/1.0, который описан в RfC1945 www.w3.org/Protocols/rfc1945/rfc1945 и датирован маем 1996 года. Для получения нужной страницы достаточно было подключиться к серверу и отправить одну строку:
При обращении к прокси-серверу необходимо было использовать не абсолютный путь, а полный адрес:
Это всё описано в разделе 5.1.2 «Request-URI».
Чтобы один сервер мог обслуживать сразу несколько доменных имён создатели протокола добавили заголовок запроса «Host:», который должен был содержать домен, к которому идёт обращение. Хотя данный заголовок и не является частью стандарта HTTP/1.0, но некоторыми серверами и клиентами он стал поддерживаться. Например, wget отправляет запросы по протоколу HTTP/1.0, но добавляет «Host:».
В июне 1999 года (четырнадцать лет назад) появился протокол HTTP/1.1, который описан в RfC2616 www.w3.org/Protocols/rfc2616/rfc2616.html. В разделе 14.23 новый протокол потребовал, чтобы каждый заголовок запроса содержал поле «Host»:
Кроме этого значительные изменения коснулись Request-URI из строки запросов (раздел 5.1.2). Как и в предыдущем протоколе полный адрес требуется при запросах к прокси серверам («The absoluteURI form is REQUIRED when the request is being made to a proxy.»). Но отвечать на подобные запросы должны все сервера, хотя оформлять подобные запросы клиенты будут лишь к прокси серверам:
Обращаю внимание, что предполагался переход на полные адреса (absoluteURI, например, http://www.w3.org/pub/WWW/TheProject.html), поэтому от клиентов не требуется обязательного использования лишь абсолютных путей (abs_path, например, /pub/WWW/TheProject.html). Кроме того, от сервера в явном виде требуется умение отвечать на запросы клиентов с absoluteURI, поэтому возражение, что в данном случае запрос клиента не является корректным, исключаю сразу, поскольку «клиент всегда прав».
Изменения в Request-URI могут показаться безобидными, но раздел 5.2 содержит одно важное требование: «If Request-URI is an absoluteURI, the host is part of the Request-URI. Any Host header field value in the request MUST be ignored.» То есть интерпретация запроса
должна совпадать с запросом
Вы игнорируете «Host:» при запросах с absoluteURI?
В документации по mod_rewrite написано следующее:
То есть в %{REQUEST_URI} всегда будет абсолютный путь и никогда полного адреса.
Попробуйте стандартную SEO задачу по добавлению «www» к домену без него решить с помощью mod_rewrite, если пользователь отправит следующий запрос:
Вначале статьи спрашивал про отличие %{REQUEST_URI} в Apache mod_rewrite от $_SERVER["REQUEST_URI"] в PHP, поэтому приведу выдержку из документации к PHP:
Может быть это где-нибудь и настраивается, но у меня PHP/5.3.13 возвращает absoluteURI при запросе с полным адресом.
Давайте теперь рассмотрим, что же происходит при запросах к реальным серверам. Адреса сайтов взял со страницы компаний Хабра (там список меняется, брал в конце прошлой недели). Набросал небольшой скрипт на Node.JS, в котором функция http_check отправляет одиночные запросы, а full_http_check формирует к одному серверу несколько запросов по определённым шаблонам.
Теперь рассмотрим подробнее каждый из шаблонов и реакцию сайтов.
Самый распространённый вариант запроса HTTP/1.1, включающий абсолютный путь и правильный заголовок Host. На него должен корректно отвечать любой сервер, то есть ждём «HTTP/1.1 200 OK».
Все сервера вернули «HTTP/1.1 200 OK». Ниже представлена таблица значений заголовка ответа «Server»:
Вариант первого типа запросов, но вместо абсолютного пути указываем полный адрес.
В ответ на этот запрос все сервера опять проявили единодушие. «Лёгкие» запросы разбирать каждый сервер умеет.
Запрос на HTTP/1.0 с абсолютным путём, без «Host:». Должны получить «HTTP/1.0 200 OK».
На третьем запросе сервера «посыпались». И нет ни одного ответа «HTTP/1.0 200 OK».
Предыдущий запрос, но добавим «Host:». От первого запроса отличается лишь версией протокола.
Очень положительным образом подействовал Host на сервера — у всех ответ «200 OK», но HTTP/1.0 был лишь у следующих: Intel и KolibriOS Project Team.
Запрос на HTTP/1.0 с полным адресом, без «Host:». Было бы здорово прочитать «HTTP/1.0 200 OK».
Картина полностью совпадает с результатами предыдущего запроса, но вот e-Legion Ltd. выдал «HTTP/1.1 500 INTERNAL SERVER ERROR».
Предыдущий запрос, но добавим «Host:». От второго запроса отличается лишь версией протокола.
Результаты полностью совпадают с четвёртым запросом, то есть «Host:» исправил внутреннюю ошибку у сервера e-Legion Ltd.
Вариант второго запроса с полным адресом, но в «Host:» запишем несуществующий поддомен. Запрос абсолютно корректный, поэтому сервер должен отвечать «HTTP/1.1 200 OK».
Теперь в качестве «Host:» укажем несуществующий домен. В запросе ничего не изменилось, но некоторым серверам это может уже не понравиться.
Заголовок «Host:» должен полностью игнорироваться, поэтому запишем произвольный текст, которому позавидуют многие пароли. По стандарту будем ожидать «HTTP/1.1 200 OK».
На запросы 7-9 сервера отвечали одинаково следующим образом:
Первый из неправильных запросов. Отправим правильный «Host:», но в полном адресе добавим несуществующий поддомен.
Поскольку начались запросы с ошибками, то результаты пугать не должны.
Почти треть серверов не стала тратить время на попытку подсказать правильный путь (перенаправить). К сожалению, многие сервера просто перенаправляют на главную страницу.
Теперь попробуем отправить несуществующий домен.
Здесь результаты полностью совпадают с предыдущим запросом, но Мосигра вместо «HTTP/1.1 301 Moved Permanently» выдала уже «HTTP/1.1 404 Not Found».
А сработает ли вообще произвольный текст в качестве домена?
Ответ «HTTP/1.1 200 OK» пришёл от Intel и Opera Software ASA. IBM и Мосигра вернули «HTTP/1.1 404 Not Found». Все остальные написали 404 Bad Request, причём часть вообще без заголовка (возможный вариант в HTTP/1.0).
Копия одинадцатого запроса, но ещё и с поддоменом в качестве «Host:». Вряд ли имеет смысл проверять другие некорректные комбинации.
Результаты тоже стали копией запроса 11, но сдался Intel и вернул «HTTP/1.0 400 Bad Request».
Второй запрос, но воспользуемся несуществующим протоколом при указании полного адреса. Здесь-то уже точно должна быть ошибка.
Оказалось, что довольно много сайтов воспринимают протокол HABR:
Попробуем окончательно сломить сопротивление сервера и отправим предыдущий запрос, но с некорректным поддоменом.
Результаты похожи на десятый запрос, но есть и изменения:
Попробуем использовать произвольный домен.
Результаты совпали с предыдущим запросом.
И в третий раз попробуем заменить домен на произвольный текст.
Уже ни одного положительного ответа от сервера. По сравнению с запросом 12 изменения есть у следующих сайтов:
А теперь попробуем избавиться и от корректного заголовка «Host:».
Всего одно изменение по сравнению с предыдущим результатом — сервер KolibriOS Project Team стал возвращать «HTTP/1.1 404 Not Found» вместо «HTTP/1.1 301 Moved Permanently».
Напишите, если хотите попробовать какие-нибудь ещё варианты запросов. А можете сделать это и сами.
Попробуем подвести некоторые итоги. Почти все рассмотренные сервера корректно отвечали на HTTP/1.1 запросы. Исключение составили DevConf, e-Legion Ltd. и Intel. Первые два используют nginx, поэтому проблема, скорее всего, именно в его настройке. Intel же использует AkamaiGHost, который либо неправильно настроен, либо плохо поддерживает HTTP/1.1. Допускаю, что одной из причин корректного прохождения тестов является именно nginx (его использовали 14 из 19 серверов). Из-за разницы в версиях обнаружилась цепочка из nginx/1.0.10 и nginx/1.4.1 у UIDG.
Считаете, что всё просто? Попробуйте настроить Apache с учётом SEO так, чтобы он корректно обрабатывал запросы с ошибочным «Host:» и основывался лишь на полном адресе в строке запроса.
Какой практический смысл от «неправильных» корректных запросов? Сомневаюсь, что получится найти какую-нибудь уязвимость. Но неужели почти за пятнадцать лет никто не научился создавать корректные HTTP/1.1 сервера?
P.S. Помните про различия между %{REQUEST_URI} в Apache mod_rewrite и $_SERVER["REQUEST_URI"] в PHP.
UPD1:
По совету AEP взял второй запрос, но к хосту добавил ещё нулевой байт и некоторую строку. Тут зависело от того, насколько хорошо сервер будет игнорировать хост с нулевым байтом.
В скрипт добавил следующий шаблон:
Все сервера вернули «HTTP/1.1 400 Bad Request», кроме IBM, Opera Software ASA и Мосигра.
Когда попробовал нулевой байт добавить в запрос, то кроме IBM и Opera Software все сообщили об ошибке 400.
Сможете в .htaccess на уровне Apache сделать корректную переадресацию 301 с домена с префиксом www или на него?
Для последнего вопроса я и сейчас не смогу предложить решение. Причина в протоколе HTTP/1.1, который пришлось изучить подробнее, когда «изобретал велосипед» (создавал ядро для сайта).
Всё дело в HTTP-заголовке запроса «Host:». При определённых условиях там может быть всё, что угодно, причём сервер должен полностью это проигнорировать согласно HTTP/1.1. Большинство же разработчиков используют значение этого поля, например, для SEO-оптимизаций. Забегая вперёд, скажу, что дополнительный прокси (например, nginx) позволит решить эту проблему.
Для иллюстрации некорректного поведения серверов решил перебрать сайты компаний Хабра. Для дюжины сайтов сделал это вручную, а потом обнаружил, что некоторые сайты на ошибочные запросы отвечают «правильно». После этого была написана небольшая утилита для тестирования, что позволило увеличить количество тестовых шаблонов и проверяемых сайтов.
Что же скрывает REQUEST_URI в HTTP/1.1?
Теория
HTTP/1.0
Начну с протокола HTTP/1.0, который описан в RfC1945 www.w3.org/Protocols/rfc1945/rfc1945 и датирован маем 1996 года. Для получения нужной страницы достаточно было подключиться к серверу и отправить одну строку:
GET /path/to/resource.html HTTP/1.0
При обращении к прокси-серверу необходимо было использовать не абсолютный путь, а полный адрес:
GET http://domain.name/path/to/resource.html HTTP/1.0
Это всё описано в разделе 5.1.2 «Request-URI».
Появление Host
Чтобы один сервер мог обслуживать сразу несколько доменных имён создатели протокола добавили заголовок запроса «Host:», который должен был содержать домен, к которому идёт обращение. Хотя данный заголовок и не является частью стандарта HTTP/1.0, но некоторыми серверами и клиентами он стал поддерживаться. Например, wget отправляет запросы по протоколу HTTP/1.0, но добавляет «Host:».
HTTP/1.1
В июне 1999 года (четырнадцать лет назад) появился протокол HTTP/1.1, который описан в RfC2616 www.w3.org/Protocols/rfc2616/rfc2616.html. В разделе 14.23 новый протокол потребовал, чтобы каждый заголовок запроса содержал поле «Host»:
A client MUST include a Host header field in all HTTP/1.1 request messages. If the requested URI does not include an Internet host name for the service being requested, then the Host header field MUST be given with an empty value.
Кроме этого значительные изменения коснулись Request-URI из строки запросов (раздел 5.1.2). Как и в предыдущем протоколе полный адрес требуется при запросах к прокси серверам («The absoluteURI form is REQUIRED when the request is being made to a proxy.»). Но отвечать на подобные запросы должны все сервера, хотя оформлять подобные запросы клиенты будут лишь к прокси серверам:
To allow for transition to absoluteURIs in all requests in future versions of HTTP, all HTTP/1.1 servers MUST accept the absoluteURI form in requests, even though HTTP/1.1 clients will only generate them in requests to proxies.
Обращаю внимание, что предполагался переход на полные адреса (absoluteURI, например, http://www.w3.org/pub/WWW/TheProject.html), поэтому от клиентов не требуется обязательного использования лишь абсолютных путей (abs_path, например, /pub/WWW/TheProject.html). Кроме того, от сервера в явном виде требуется умение отвечать на запросы клиентов с absoluteURI, поэтому возражение, что в данном случае запрос клиента не является корректным, исключаю сразу, поскольку «клиент всегда прав».
Host в HTTP/1.1
Изменения в Request-URI могут показаться безобидными, но раздел 5.2 содержит одно важное требование: «If Request-URI is an absoluteURI, the host is part of the Request-URI. Any Host header field value in the request MUST be ignored.» То есть интерпретация запроса
GET http://domain.name/path/to/resource.html HTTP/1.1 Host: любой_текст_тут
должна совпадать с запросом
GET /path/to/resource.html HTTP/1.1 Host: domain.name
Вы игнорируете «Host:» при запросах с absoluteURI?
%{REQUEST_URI} и $_SERVER["REQUEST_URI"]
В документации по mod_rewrite написано следующее:
THE_REQUEST
The full HTTP request line sent by the browser to the server (e.g., «GET /index.html HTTP/1.1»). This does not include any additional headers sent by the browser. This value has not been unescaped (decoded), unlike most other variables below.
REQUEST_URI
The path component of the requested URI, such as "/index.html". This notably excludes the query string which is available as as its own variable named QUERY_STRING.
То есть в %{REQUEST_URI} всегда будет абсолютный путь и никогда полного адреса.
Попробуйте стандартную SEO задачу по добавлению «www» к домену без него решить с помощью mod_rewrite, если пользователь отправит следующий запрос:
GET http://domain.name/path/to/resource.html HTTP/1.1 Host: www.domain.name
Вначале статьи спрашивал про отличие %{REQUEST_URI} в Apache mod_rewrite от $_SERVER["REQUEST_URI"] в PHP, поэтому приведу выдержку из документации к PHP:
REQUEST_URI
The URI which was given in order to access this page; for instance, '/index.html'.
Может быть это где-нибудь и настраивается, но у меня PHP/5.3.13 возвращает absoluteURI при запросе с полным адресом.
Практика
Давайте теперь рассмотрим, что же происходит при запросах к реальным серверам. Адреса сайтов взял со страницы компаний Хабра (там список меняется, брал в конце прошлой недели). Набросал небольшой скрипт на Node.JS, в котором функция http_check отправляет одиночные запросы, а full_http_check формирует к одному серверу несколько запросов по определённым шаблонам.
код скрипта
var net = require('net');
var default_result = function(title) {
if (title) {
return {'title': 'title', 'step': 'step', 'host': 'host', 'request': 'request', 'header': 'header', 'full_response': 'full_response', 'response': 'response', 'server': 'server', 'length': 'length', 'location': 'location', 'error': 'error'};
} else {
return {'title': '', 'step': '', 'host': '', 'request': '', 'header': '', 'full_response': '', 'response': '', 'server': '', 'length': '', 'location': '', 'error': ''};
}
};
var format_result = function(result) {
return '' + result['title'].toString() + '\t'
+ result['step'] + '\t'
+ result['host'] + '\t'
+ result['request'].toString() + '\t'
+ result['header'].toString() + '\t'
+ result['response'].toString() + '\t'
+ result['server'].toString() + '\t'
+ result['length'].toString() + '\t'
+ result['error'].toString() + '\t'
+ result['location'].toString() + '\t'
+ result['full_response'].toString();
};
var http_check = function(title, step, host, req, host_hdr)
{
var host_header = host_hdr || '';
var result = default_result(false);
result['title'] = title;
result['step'] = step;
result['host'] = host;
result['request'] = req;
result['header'] = host_header;
var dat = '';
var client = net.connect({port: 80, host: host},
function() { //'connect' listener
client.on('data', function (data) {
dat = dat + data;
var lines = dat.toString().split('\r\n');
result['full_response'] = JSON.stringify(dat.toString().split('\r\n\r\n')[0]);
result['response'] = lines[0] || false;
if (lines[0].substring(0, 5) == 'HTTP/') {
var i = 1;
while (lines[i] != '') {
var title = lines[i].match(/^([^:]+:)\s(.+)$/);
if (title[1] == 'Location:') {
result['location'] = title[2];
} else if (title[1] == 'Server:') {
result['server'] = title[2];
} else if (title[1] == 'Content-Length:') {
result['length'] = title[2];
}
i++;
}
if (dat.indexOf('\r\n\r\n') >= 0) {
client.end();
client.destroy();
}
} else {
client.end();
client.destroy();
}
});
client.on('end', function () {
console.log('client disconnected');
});
client.on('error', function (error) {
console.log('ERROR: ' + error.toString());
});
client.on('timeout', function () {
console.log('Timeout');
});
client.on('close', function (had_error) {
result['error'] = result['error'] || had_error || '';
console.log(format_result(result));
});
client.write(req + '\r\n');
host_hdr && client.write('Host: ' + host_hdr + '\r\n');
client.write('\r\n');
});
};
var full_http_check = function(title, url) {
var parts = url.match(/^http:\/\/([^\/]+)(.+)$/);
// 1
// GET /path/to/resource.html HTTP/1.1
// Host: domain.name
http_check(title, '01', parts[1], 'GET ' + parts[2] + ' HTTP/1.1', parts[1]);
// 2
// GET http://domain.name/path/to/resource.html HTTP/1.1
// Host: domain.name
http_check(title, '02', parts[1], 'GET http://' + parts[1] + parts[2] + ' HTTP/1.1', parts[1]);
// 3
// GET /path/to/resource.html HTTP/1.0
http_check(title, '03', parts[1], 'GET ' + parts[2] + ' HTTP/1.0', '');
// 4
// GET /path/to/resource.html HTTP/1.0
// Host: domain.name
http_check(title, '04', parts[1], 'GET ' + parts[2] + ' HTTP/1.0', parts[1]);
// 5
// GET http://domain.name/path/to/resource.html HTTP/1.0
http_check(title, '05', parts[1], 'GET http://' + parts[1] + parts[2] + ' HTTP/1.0', '');
// 6
// GET http://domain.name/path/to/resource.html HTTP/1.0
// Host: domain.name
http_check(title, '06', parts[1], 'GET http://' + parts[1] + parts[2] + ' HTTP/1.0', parts[1]);
// 7
// GET http://domain.name/path/to/resource.html HTTP/1.1
// Host: void.domain.name
http_check(title, '07', parts[1], 'GET http://' + parts[1] + parts[2] + ' HTTP/1.1', 'void.' + parts[1]);
// 8
// GET http://domain.name/path/to/resource.html HTTP/1.1
// Host: local.fake
http_check(title, '08', parts[1], 'GET http://' + parts[1] + parts[2] + ' HTTP/1.1', 'local.fake');
// 9
// GET http://domain.name/path/to/resource.html HTTP/1.1
// Host: l-IjFN=fiG(w+J2p:#.{92!m`d^?
http_check(title, '09', parts[1], 'GET http://' + parts[1] + parts[2] + ' HTTP/1.1', 'l-IjFN=fiG(w+J2p:#.{92!m`d^?');
// 10
// GET http://fake.domain.name/path/to/resource.html HTTP/1.1
// Host: domain.name
http_check(title, '10', parts[1], 'GET http://fake.' + parts[1] + parts[2] + ' HTTP/1.1', parts[1]);
// 11
// GET http://local.fake/path/to/resource.html HTTP/1.1
// Host: domain.name
http_check(title, '11', parts[1], 'GET http://local.fake' + parts[2] + ' HTTP/1.1', parts[1]);
// 12
// GET http://l-IjFN=fiG(w+J2p:#.{92!m`d^?/path/to/resource.html HTTP/1.1
// Host: domain.name
http_check(title, '12', parts[1], 'GET http://l-IjFN=fiG(w+J2p:#.{92!m`d^?' + parts[2] + ' HTTP/1.1', parts[1]);
// 13
// GET http://local.fake/path/to/resource.html HTTP/1.1
// Host: void.domain.name
http_check(title, '13', parts[1], 'GET http://local.fake' + parts[2] + ' HTTP/1.1', 'void.' + parts[1]);
// 14
// GET habr://domain.name/path/to/resource.html HTTP/1.1
// Host: domain.name
http_check(title, '14', parts[1], 'GET habr://' + parts[1] + parts[2] + ' HTTP/1.1', parts[1]);
// 15
// GET habr://void.domain.name/path/to/resource.html HTTP/1.1
// Host: domain.name
http_check(title, '15', parts[1], 'GET habr://void.' + parts[1] + parts[2] + ' HTTP/1.1', parts[1]);
// 16
// GET habr://local.fake/path/to/resource.html HTTP/1.1
// Host: domain.name
http_check(title, '16', parts[1], 'GET habr://local.fake' + parts[2] + ' HTTP/1.1', parts[1]);
// 17
// GET habr://l-IjFN=fiG(w+J2p:#.{92!m`d^?/path/to/resource.html HTTP/1.1
// Host: domain.name
http_check(title, '17', parts[1], 'GET habr://l-IjFN=fiG(w+J2p:#.{92!m`d^?' + parts[2] + ' HTTP/1.1', parts[1]);
// 18
// GET habr://l-IjFN=fiG(w+J2p:#.{92!m`d^?/path/to/resource.html HTTP/1.1
// Host: local.fake
http_check(title, '18', parts[1], 'GET habr://l-IjFN=fiG(w+J2p:#.{92!m`d^?' + parts[2] + ' HTTP/1.1', 'local.fake');
};
console.log(format_result(default_result(true)));
/*
http_check('IBM Fake', 'www.ibm.com', 'GET ttp://com/midmarket/ru/ru/ HTTP/1.1', 'ibm');
full_http_check('IBM', 'http://www.ibm.com/midmarket/ru/ru/');
*/
full_http_check('Яндекс', 'http://company.yandex.ru/about/main/');
full_http_check('JetBrains', 'http://www.jetbrains.com/products.html');
full_http_check('Box Overview', 'http://7del.net/texts/galaxy-note.html');
full_http_check('KolibriOS Project Team', 'http://kolibrios.org/en/download.htm');
full_http_check('Opera Software ASA', 'http://www.opera.com/about');
full_http_check('Apps4All', 'http://apps4all.ru/news/apple/apple-ios-7-beta.html');
full_http_check('Нордавинд', 'http://nordavind.ru/node/207');
full_http_check('Mail.Ru Group', 'http://corp.mail.ru/about/');
full_http_check('Microsoft', 'http://windows.microsoft.com/ru-RU/windows/home');
full_http_check('Zfort Group', 'http://www.zfort.com.ua/company/about/');
full_http_check('IBM', 'http://www.ibm.com/contact/ru/ru/');
full_http_check('UIDG', 'http://uidesign.ru/about/');
full_http_check('Intel', 'http://www.intel.ru/content/www/ru/ru/company-overview/company-overview.html');
full_http_check('Rusonyx', 'http://www.rusonyx.ru/company/reasons/');
full_http_check('Мосигра', 'http://www.mosigra.ru/page/about/');
full_http_check('DevConf', 'http://devconf.ru/about/');
full_http_check('e-Legion Ltd.', 'http://www.e-legion.ru/contacts/');
full_http_check('Badoo', 'http://corp.badoo.com/company/');
full_http_check('ВымпелКом (Билайн)', 'http://mobile.beeline.ru/msk/setup/index.wbp');
Теперь рассмотрим подробнее каждый из шаблонов и реакцию сайтов.
Запрос 1
Самый распространённый вариант запроса HTTP/1.1, включающий абсолютный путь и правильный заголовок Host. На него должен корректно отвечать любой сервер, то есть ждём «HTTP/1.1 200 OK».
GET /path/to/resource.html HTTP/1.1 Host: domain.name
Все сервера вернули «HTTP/1.1 200 OK». Ниже представлена таблица значений заголовка ответа «Server»:
Компания | Заголовок «Server:» |
---|---|
Apps4All | nginx/1.0.15 |
Badoo | nginx |
Box Overview | nginx/1.2.1 |
DevConf | nginx/1.0.15 |
e-Legion Ltd. | nginx/1.0.5 |
IBM | IBM_HTTP_Server |
Intel | Microsoft-IIS/7.5 |
JetBrains | nginx |
KolibriOS Project Team | lighttpd/1.4.32 |
Mail.Ru Group | nginx/1.2.5 |
Microsoft | Microsoft-IIS/7.5 |
Opera Software ASA | nginx |
Rusonyx | nginx |
UIDG | Apache |
Zfort Group | nginx/1.4.1 |
ВымпелКом (Билайн) | Microsoft-IIS/7.5 |
Мосигра | nginx/1.4.1 |
Нордавинд | nginx/1.0.4 |
Яндекс | nginx/1.2.1 |
Запрос 2
Вариант первого типа запросов, но вместо абсолютного пути указываем полный адрес.
GET http://domain.name/path/to/resource.html HTTP/1.1 Host: domain.name
В ответ на этот запрос все сервера опять проявили единодушие. «Лёгкие» запросы разбирать каждый сервер умеет.
Запрос 3
Запрос на HTTP/1.0 с абсолютным путём, без «Host:». Должны получить «HTTP/1.0 200 OK».
GET /path/to/resource.html HTTP/1.0
На третьем запросе сервера «посыпались». И нет ни одного ответа «HTTP/1.0 200 OK».
Компания | Ответ сервера |
---|---|
Apps4All | HTTP/1.1 301 Moved Permanently |
Badoo | HTTP/1.1 302 Moved Temporarily |
Box Overview | HTTP/1.1 200 OK |
DevConf | HTTP/1.1 404 Not Found |
e-Legion Ltd. | HTTP/1.1 301 Moved Permanently |
IBM | HTTP/1.1 200 OK |
Intel | HTTP/1.0 400 Bad Request |
JetBrains | HTTP/1.1 301 Moved Permanently |
KolibriOS Project Team | HTTP/1.0 404 Not Found |
Mail.Ru Group | HTTP/1.1 200 OK |
Microsoft | HTTP/1.1 200 OK |
Opera Software ASA | HTTP/1.1 404 Not Found |
Rusonyx | HTTP/1.1 301 Moved Permanently |
UIDG | HTTP/1.1 404 Not Found |
Zfort Group | HTTP/1.1 404 Not Found |
ВымпелКом (Билайн) | HTTP/1.1 302 Redirect |
Мосигра | HTTP/1.1 404 Not Found |
Нордавинд | HTTP/1.1 200 OK |
Яндекс | HTTP/1.1 404 Not Found |
Запрос 4
Предыдущий запрос, но добавим «Host:». От первого запроса отличается лишь версией протокола.
GET /path/to/resource.html HTTP/1.0 Host: domain.name
Очень положительным образом подействовал Host на сервера — у всех ответ «200 OK», но HTTP/1.0 был лишь у следующих: Intel и KolibriOS Project Team.
Запрос 5
Запрос на HTTP/1.0 с полным адресом, без «Host:». Было бы здорово прочитать «HTTP/1.0 200 OK».
GET http://domain.name/path/to/resource.html HTTP/1.0
Картина полностью совпадает с результатами предыдущего запроса, но вот e-Legion Ltd. выдал «HTTP/1.1 500 INTERNAL SERVER ERROR».
Запрос 6
Предыдущий запрос, но добавим «Host:». От второго запроса отличается лишь версией протокола.
GET http://domain.name/path/to/resource.html HTTP/1.0 Host: domain.name
Результаты полностью совпадают с четвёртым запросом, то есть «Host:» исправил внутреннюю ошибку у сервера e-Legion Ltd.
Запрос 7
Вариант второго запроса с полным адресом, но в «Host:» запишем несуществующий поддомен. Запрос абсолютно корректный, поэтому сервер должен отвечать «HTTP/1.1 200 OK».
GET http://domain.name/path/to/resource.html HTTP/1.1 Host: void.domain.name
Запрос 8
Теперь в качестве «Host:» укажем несуществующий домен. В запросе ничего не изменилось, но некоторым серверам это может уже не понравиться.
GET http://domain.name/path/to/resource.html HTTP/1.1 Host: local.fake
Запрос 9
Заголовок «Host:» должен полностью игнорироваться, поэтому запишем произвольный текст, которому позавидуют многие пароли. По стандарту будем ожидать «HTTP/1.1 200 OK».
GET http://domain.name/path/to/resource.html HTTP/1.1 Host: l-IjFN=fiG(w+J2p:#.{92!m`d^?
На запросы 7-9 сервера отвечали одинаково следующим образом:
Компания | Ответ сервера | Заголовок «Server:» |
---|---|---|
Apps4All | HTTP/1.1 200 OK | nginx/1.0.15 |
Badoo | HTTP/1.1 200 OK | nginx |
Box Overview | HTTP/1.1 200 OK | nginx/1.2.1 |
DevConf | HTTP/1.1 500 Internal Server Error | nginx/1.0.15 |
e-Legion Ltd. | HTTP/1.1 500 INTERNAL SERVER ERROR | nginx/1.0.5 |
IBM | HTTP/1.1 200 OK | IBM_HTTP_Server |
Intel | HTTP/1.0 400 Bad Request | AkamaiGHost |
JetBrains | HTTP/1.1 200 OK | nginx |
KolibriOS Project Team | HTTP/1.1 200 OK | lighttpd/1.4.32 |
Mail.Ru Group | HTTP/1.1 200 OK | nginx/1.2.5 |
Microsoft | HTTP/1.1 200 OK | Microsoft-IIS/7.5 |
Opera Software ASA | HTTP/1.1 200 OK | nginx |
Rusonyx | HTTP/1.1 200 OK | nginx |
UIDG | HTTP/1.1 200 OK | Apache |
Zfort Group | HTTP/1.1 200 OK | nginx/1.4.1 |
ВымпелКом (Билайн) | HTTP/1.1 200 OK | Microsoft-IIS/7.5 |
Мосигра | HTTP/1.1 200 OK | nginx/1.4.1 |
Нордавинд | HTTP/1.1 200 OK | nginx/1.0.4 |
Яндекс | HTTP/1.1 200 OK | nginx/1.2.1 |
Запрос 10
Первый из неправильных запросов. Отправим правильный «Host:», но в полном адресе добавим несуществующий поддомен.
GET http://fake.domain.name/path/to/resource.html HTTP/1.1 Host: domain.name
Поскольку начались запросы с ошибками, то результаты пугать не должны.
Компания | Ответ сервера |
---|---|
Apps4All | HTTP/1.1 301 Moved Permanently |
Badoo | HTTP/1.1 301 Moved Permanently |
Box Overview | HTTP/1.1 200 OK |
DevConf | HTTP/1.1 404 Not Found |
e-Legion Ltd. | HTTP/1.1 301 Moved Permanently |
IBM | HTTP/1.1 200 OK |
Intel | HTTP/1.1 200 OK |
JetBrains | HTTP/1.1 301 Moved Permanently |
KolibriOS Project Team | HTTP/1.1 404 Not Found |
Mail.Ru Group | HTTP/1.1 200 OK |
Microsoft | HTTP/1.1 200 OK |
Opera Software ASA | HTTP/1.1 404 Not Found |
Rusonyx | HTTP/1.1 301 Moved Permanently |
UIDG | HTTP/1.1 404 Not Found |
Zfort Group | HTTP/1.1 404 Not Found |
ВымпелКом (Билайн) | HTTP/1.1 302 Redirect |
Мосигра | HTTP/1.1 301 Moved Permanently |
Нордавинд | HTTP/1.1 200 OK |
Яндекс | HTTP/1.1 404 Not Found |
Почти треть серверов не стала тратить время на попытку подсказать правильный путь (перенаправить). К сожалению, многие сервера просто перенаправляют на главную страницу.
Запрос 11
Теперь попробуем отправить несуществующий домен.
GET http://local.fake/path/to/resource.html HTTP/1.1 Host: domain.name
Здесь результаты полностью совпадают с предыдущим запросом, но Мосигра вместо «HTTP/1.1 301 Moved Permanently» выдала уже «HTTP/1.1 404 Not Found».
Запрос 12
А сработает ли вообще произвольный текст в качестве домена?
GET http://l-IjFN=fiG(w+J2p:#.{92!m`d^?/path/to/resource.html HTTP/1.1 Host: domain.name
Ответ «HTTP/1.1 200 OK» пришёл от Intel и Opera Software ASA. IBM и Мосигра вернули «HTTP/1.1 404 Not Found». Все остальные написали 404 Bad Request, причём часть вообще без заголовка (возможный вариант в HTTP/1.0).
Запрос 13
Копия одинадцатого запроса, но ещё и с поддоменом в качестве «Host:». Вряд ли имеет смысл проверять другие некорректные комбинации.
GET http://local.fake/path/to/resource.html HTTP/1.1 Host: void.domain.name
Результаты тоже стали копией запроса 11, но сдался Intel и вернул «HTTP/1.0 400 Bad Request».
Запрос 14
Второй запрос, но воспользуемся несуществующим протоколом при указании полного адреса. Здесь-то уже точно должна быть ошибка.
GET habr://domain.name/path/to/resource.html HTTP/1.1 Host: domain.name
Оказалось, что довольно много сайтов воспринимают протокол HABR:
Компания | Ответ сервера |
---|---|
Apps4All | HTTP/1.1 200 OK |
Badoo | HTTP/1.1 200 OK |
Box Overview | HTTP/1.1 200 OK |
DevConf | HTTP/1.1 200 OK |
e-Legion Ltd. | HTTP/1.1 200 OK |
IBM | HTTP/1.1 200 OK |
Intel | HTTP/1.0 400 Bad Request |
JetBrains | HTTP/1.1 200 OK |
KolibriOS Project Team | HTTP/1.1 301 Moved Permanently |
Mail.Ru Group | HTTP/1.1 200 OK |
Microsoft | HTTP/1.1 400 Bad Request |
Opera Software ASA | HTTP/1.1 400 BAD_REQUEST |
Rusonyx | HTTP/1.1 200 OK |
UIDG | HTTP/1.1 200 OK |
Zfort Group | HTTP/1.1 200 OK |
ВымпелКом (Билайн) | HTTP/1.1 400 Bad Request |
Мосигра | HTTP/1.1 400 BAD_REQUEST |
Нордавинд | HTTP/1.1 200 OK |
Яндекс | HTTP/1.1 200 OK |
Запрос 15
Попробуем окончательно сломить сопротивление сервера и отправим предыдущий запрос, но с некорректным поддоменом.
GET habr://void.domain.name/path/to/resource.html HTTP/1.1 Host: domain.name
Результаты похожи на десятый запрос, но есть и изменения:
Компания | Запрос 10 | Запрос 15 |
---|---|---|
Apps4All | HTTP/1.1 301 Moved Permanently | HTTP/1.1 301 Moved Permanently |
Badoo | HTTP/1.1 301 Moved Permanently | HTTP/1.1 301 Moved Permanently |
Box Overview | HTTP/1.1 200 OK | HTTP/1.1 200 OK |
DevConf | HTTP/1.1 404 Not Found | HTTP/1.1 404 Not Found |
e-Legion Ltd. | HTTP/1.1 301 Moved Permanently | HTTP/1.1 301 Moved Permanently |
IBM | HTTP/1.1 200 OK | HTTP/1.1 200 OK |
Intel | HTTP/1.1 200 OK | HTTP/1.0 400 Bad Request |
JetBrains | HTTP/1.1 301 Moved Permanently | HTTP/1.1 301 Moved Permanently |
KolibriOS Project Team | HTTP/1.1 404 Not Found | HTTP/1.1 301 Moved Permanently |
Mail.Ru Group | HTTP/1.1 200 OK | HTTP/1.1 200 OK |
Microsoft | HTTP/1.1 200 OK | HTTP/1.1 400 Bad Request |
Opera Software ASA | HTTP/1.1 404 Not Found | HTTP/1.1 400 BAD_REQUEST |
Rusonyx | HTTP/1.1 301 Moved Permanently | HTTP/1.1 301 Moved Permanently |
UIDG | HTTP/1.1 404 Not Found | HTTP/1.1 404 Not Found |
Zfort Group | HTTP/1.1 404 Not Found | HTTP/1.1 404 Not Found |
ВымпелКом (Билайн) | HTTP/1.1 302 Redirect | HTTP/1.1 400 Bad Request |
Мосигра | HTTP/1.1 301 Moved Permanently | HTTP/1.1 400 BAD_REQUEST |
Нордавинд | HTTP/1.1 200 OK | HTTP/1.1 200 OK |
Яндекс | HTTP/1.1 404 Not Found | HTTP/1.1 404 Not Found |
Запрос 16
Попробуем использовать произвольный домен.
GET habr://local.fake/path/to/resource.html HTTP/1.1 Host: domain.name
Результаты совпали с предыдущим запросом.
Запрос 17
И в третий раз попробуем заменить домен на произвольный текст.
GET habr://l-IjFN=fiG(w+J2p:#.{92!m`d^?/path/to/resource.html HTTP/1.1 Host: domain.name
Уже ни одного положительного ответа от сервера. По сравнению с запросом 12 изменения есть у следующих сайтов:
Компания | Запрос 12 | Запрос 17 |
---|---|---|
Intel | HTTP/1.1 200 OK | HTTP/1.0 400 Bad Request |
KolibriOS Project Team | HTTP/1.1 400 Bad Request | HTTP/1.1 301 Moved Permanently |
Opera Software ASA | HTTP/1.1 200 OK | HTTP/1.1 400 BAD_REQUEST |
Мосигра | HTTP/1.1 404 Not Found | HTTP/1.1 400 BAD_REQUEST |
Запрос 18
А теперь попробуем избавиться и от корректного заголовка «Host:».
GET habr://l-IjFN=fiG(w+J2p:#.{92!m`d^?/path/to/resource.html HTTP/1.1 Host: local.fake
Всего одно изменение по сравнению с предыдущим результатом — сервер KolibriOS Project Team стал возвращать «HTTP/1.1 404 Not Found» вместо «HTTP/1.1 301 Moved Permanently».
Запрос N
Напишите, если хотите попробовать какие-нибудь ещё варианты запросов. А можете сделать это и сами.
Заключение
Попробуем подвести некоторые итоги. Почти все рассмотренные сервера корректно отвечали на HTTP/1.1 запросы. Исключение составили DevConf, e-Legion Ltd. и Intel. Первые два используют nginx, поэтому проблема, скорее всего, именно в его настройке. Intel же использует AkamaiGHost, который либо неправильно настроен, либо плохо поддерживает HTTP/1.1. Допускаю, что одной из причин корректного прохождения тестов является именно nginx (его использовали 14 из 19 серверов). Из-за разницы в версиях обнаружилась цепочка из nginx/1.0.10 и nginx/1.4.1 у UIDG.
Считаете, что всё просто? Попробуйте настроить Apache с учётом SEO так, чтобы он корректно обрабатывал запросы с ошибочным «Host:» и основывался лишь на полном адресе в строке запроса.
Какой практический смысл от «неправильных» корректных запросов? Сомневаюсь, что получится найти какую-нибудь уязвимость. Но неужели почти за пятнадцать лет никто не научился создавать корректные HTTP/1.1 сервера?
P.S. Помните про различия между %{REQUEST_URI} в Apache mod_rewrite и $_SERVER["REQUEST_URI"] в PHP.
UPD1:
Запрос 19
По совету AEP взял второй запрос, но к хосту добавил ещё нулевой байт и некоторую строку. Тут зависело от того, насколько хорошо сервер будет игнорировать хост с нулевым байтом.
GET http://domain.name/path/to/resource.html HTTP/1.1 Host: domain.name{нулевой байт}fake_and_void
В скрипт добавил следующий шаблон:
http_check(title, '19', parts[1], 'GET http://' + parts[1] + parts[2] + ' HTTP/1.1', parts[1] + '\0fake_and_void_text');
Все сервера вернули «HTTP/1.1 400 Bad Request», кроме IBM, Opera Software ASA и Мосигра.
Когда попробовал нулевой байт добавить в запрос, то кроме IBM и Opera Software все сообщили об ошибке 400.