Почему системный файловый диалог меняет текущий каталог?

Это перевод Why does the common file dialog change the current directory? Автор: Реймонд Чен.

Когда вы перемещаетесь по папкам в системном диалоге открытия (или сохранения) файлов, то диалог вызывает функцию Set­Current­Directory, передавая ей каталог, в который вы перешли (эй, не заставляйте меня возрождать уголок зануды).

Окей, возможно, вашей первой реакцией будет: «Правда? Я вообще не знал, что он так делает!» Что ж, это ещё одна страница в истории проклятия текущего каталога.

Хорошо, но теперь возникает вопрос: «А зачем диалог это делает?»

Вообще-то, вы уже должны знать ответ на этот вопрос. Многие программы рассчитывают, что текущий каталог совпадает с каталогом открываемого документа.

Но, оказывается, существует способ сказать: «Нет, я — это не такая ламерская программа. Я не делаю предположений о текущем каталоге, я могу работать с любым. Так что не меняй текущий каталог». Вы можете сделать это, передав флаг OFN_NO­CHANGE­DIR (а если ваша программа использует интерфейс IFile­Dialog, то опция NO­CHANGE­DIR включена перманентно — ура Прогрессу!).

Прим.пер.: для Delphi флаг OFN_NO­CHANGE­DIR соответствует значению ofNoChangeDir в TCommonDialog.Options (значение fdoNoChangeDir в TCustomFileDialog.Options всегда игнорируется), а интерфейс IFile­Dialog используется Delphi в «диалогах Vista» (например, TFileOpenDialog), а также в стандартных файловых диалогах — автоматически на Vista+ при условии подключения манифеста (только в новых IDE, конечно же). В последнем случае опция ofNoChangeDir игнорируется.

Но теперь, когда вы знаете про второе проклятие текущего каталога, вы можете использовать его против первого проклятия.

Если вы смогли определить программу, удерживающую открытым каталог, и вы подозреваете, что программа пала жертвой проклятия текущего каталога, то вы можете зайти в эту программу и открыть в ней файловый диалог (например, Файл / Открыть… или Файл / Сохранить как…). В этом диалоге вы меняете папку на, скажем, корень диска или на рабочий стол. А затем закрываете (отменяете) диалог.

Поскольку системный файловый диалог изменяет текущий каталог, то вы, фактически, внедрили вызов Set­Current­Directory прямо в целевой процесс, уводя, таким образом, текущий каталог с каталога, который вы хотите удалить. Заметьте, однако, что этот трюк сработает только если это приложение не передаёт флаг OFN_NO­CHANGE­DIR в вызов Get­Save­File­Name.

Для Проводника вы можете вызвать файловый диалог нажатием Win + R и кнопки «Обзор» — и в версиях Windows вплоть до Windows XP Проводник не передаёт флаг OFN_NO­CHANGE­DIR.

Читать на сайте автора.

Ваш отладочный код может быть уязвимостью в безопасности: подгрузка опциональной отладочной DLL без указания полного пути

Это перевод Your debugging code can be a security vulnerability: Loading optional debugging DLLs without a full path. Автор: Реймонд Чен.

Запомните: плохих парней не заботит, если вы добавили код лишь для целей отладки. Если есть код — они его атакуют.

Посмотрите на такой код:

type
TDocLoadingProc = procedure(Stream: TStream);

var
OnDocLoading: TDocLoadingProc;

procedure LoadDebuggingHooks;
var
hmodDebug: HMODULE;
begin
hmodDebug := LoadLibrary('DebugHooks.dll');
if hmodDebug = 0 then
Exit;
OnDocLoading := GetProcAddress(hmodDebug, 'OnDocLoading');
end;

function LoadDocument({...}): HRESULT;
begin
// ...
if Assigned(OnDocLoading)
// Дадим отладочной ловушке изменить документ перед загрузкой
OnDocLoading(DocStream);
// ...
end;

Если вы хотите отлаживать вашу программу, вы копируете DebugHooks.dll в папку приложения. Код выше ищет эту DLL и загружает её, если она есть. Для примера я включил код одной из возможных ловушек из этой отладочной DLL. Идея примера (и это просто пример, так что давайте не будем обсуждать, хороший ли это пример) состоит в том, что непосредственно перед загрузкой документа мы вызываем функцию OnDocLoading. Эта функция может обернуть переданный ей поток в другой поток, так что содержимое потока можно логгировать при его чтении — в попытке локализовать на чём стопорится загрузка документа. Или её можно использовать для инъекции ошибок ввода-вывода с целью тестирования. В общем, используйте своё воображение.

Но этот отладочный код также содержит уязвимость.

Вспомните, что пути для поиска DLL ищут библиотеки в таком порядке:

  1. Каталог приложения;
  2. Каталог system32;
  3. Каталог system;
  4. Каталог Windows;
  5. Текущий каталог;
  6. Пути из PATH.

Когда вы отлаживаете программу, вы копируете DebugHooks.dll в папку приложения, откуда она и загружается (шаг 1 в списке выше). Но если вы не отлаживаете свою программу, то шаг 1 пропускается, и поиск продолжается дальше. DLL не будет найдена на шаге 2, 3 и 4, и в итоге мы достигаем шага 5: текущего каталога.

И теперь вас хакнули.

Как правило, ваше приложение не имеет прямого контроля над текущим каталогом. Пользователь может запустить вашу программу из любого каталога — и это каталог станет вашим текущим каталогом. И когда вызов LoadLibrary будет искать DLL в текущем каталоге, плохие парни могут положить в него их собственную DLL, ваша программа станет жертвой инъекции кода.

Это особенно опасно, если ваше приложение ассоциировано с каким-то типом файла, поскольку пользователь может запустить вашу программу просто дважды-щёлкнув по файлу, с которым вы ассоциированы.

Когда вы дважды-щёлкаете по файлу, Проводник устанавливает текущий каталог для приложения-обработчика этого файла равным каталогу, в котором расположен этот файл. Это необходимо для правильной работы приложений, которые ищут в текущем каталоге вспомогательные файлы. Например, представьте гипотетическое приложение LitWare Writer, ассоциированное с файлами *.LIT. Документ приложения LitWare Writer, представленный файлом ABC.LIT — это просто представитель семейства файлов: ABC.LIT (главный документ), ABC.LTC (индекс документа и содержание), ABC.LDC (словарь грамматики для документа), ABC.LLT (пользовательский шаблон для документа) и так далее. Когда вы открываете документ C:PROPOSALABC.LIT, LitWare Writer ищет остальные части документа в текущей папке, вместо C:PROPOSAL. Чтобы помочь таким приложениям найти все необходимые файлы, Проводник указывает функции CreateProcess, что текущий каталог для приложения LitWare Writer должен быть равен C:PROPOSAL.

Так, вы можете сказать, что программы вроде этого LitWare Writer (которые ищут файлы в текущей папке вместо папки с главным документом) плохо написаны, и я в этом с вами соглашусь. Однако Windows нужно работать даже с плохо написанными программами (упреждающее ехидное замечание: Windows сама плохо написана). В реальном мире есть слишком много плохо написанных программ, некоторые из которых являются лидерами в своих сегментах (см. выше упреждающий остроумный комментарий), и если Windows перестанет их запускать, люди скажут, что это вина Windows, а не этих программ.

Мне даже не надо закрывать глаза, чтобы представить, как нам присылают баг-отчёт с этим поведением:

«Эта программа отлично работает в MS-DOS, но в Windows она не работает. Тупая Windows».

Клиент не будет счастлив услышать следующий ответ: «Вообще-то, эта программа никогда и не работала верно, ей просто везло все эти X лет. Авторы этой программы никогда не рассматривали случай, если открываемый документ расположен вне текущего каталога. Им это сошло с рук, потому что обычно вы открываете файлы в MS-DOS следующим образом: вы переходите в каталог с документом и потом набираете LWW ABC.LIT. Но если вы выполните в MS-DOS LWW C:PROPOSALABC.LIT, то увидите то же (неверное) поведение программы. Иными словами, это поведение системы — не баг, а дизайн».

Ответ на «Это поведение по проекту» обычно звучит как «Я скажу так, что дизайн, который не даёт мне выполнять мою работу — отстойный дизайн» или кратко: «Нет, это не дизайн, это баг» (вы не верите мне, что такое происходит? Просто почитайте Slashdot).

Итак, чтобы все такие программы продолжили бы работу в Windows, система устанавливает текущий каталог для программы в каталог, содержащий открываемый документ. Это вовсе не чрезмерное и не безрассудное решение — ведь благодаря ему начала работать программа. Не то чтобы запускаемой программе было важно конкретное значение текущего каталога — ведь у неё нет контроля над ним!

Но сказанное также означает, что если вы запустили программу двойным щелчком по ассоциированному документу, то программа будет удерживать каталог с документом — потому что этот каталог будет текущим. Если только, конечно, сама программа потом не изменит свой текущий каталог.

Бонус-замечание: изначально я написал эту серию заметок более двух лет назад. Но даже тогда я не считал их чем-то удивительным, новаторским или интересным. Но, похоже, некоторые люди вновь открыли это несколько месяцев назад, и вылезли из кожи вон, чтобы присвоить себе лавры первооткрывателей. Это как новое поколение подростков, которые думают, что они изобрели секс. Чисто для протокола: вот некоторые официальные инструкции (и, чтобы расставить все точки над i: это официальные инструкции по атаке на текущий каталог, а не официальные инструкции по сексу).

Историческое замечание: почему вообще есть текущий каталог? Ответ: это идёт со времён CP/M. В CP/M не было PATH. Всё запускалось из текущего каталога. Сегодняшнее состояние вещей — это цепочка из обратных совместимостей.

Читать на сайте автора.