Как стать автором
Обновить

Двуликий REQUEST_URI или в поисках корректного HTTP/1.1 сервера

Время на прочтение15 мин
Количество просмотров67K
Вы знаете, чем отличается %{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


Начну с протокола 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.
Теги:
Хабы:
Всего голосов 48: ↑43 и ↓5+38
Комментарии27

Публикации

Истории

Работа

PHP программист
104 вакансии

Ближайшие события