Любимые (Умные) Лучшие Практики Защитного Программирования
Если бы вам пришлось выбирать свои любимые (умные) методы для защитного кодирования, какими бы они были? Хотя мои текущие языки - Java и Objective-C (с опытом работы в C++), не стесняйтесь отвечать на любом языке. Акцент здесь будет сделан на хитрых методах защиты, отличных от тех, о которых 70% из нас уже знают. Так что теперь пришло время копаться в вашей сумке трюков.
Другими словами, попробуйте думать о чем-то, кроме этого неинтересного примера:
if(5 == x)
вместоif(x == 5)
: чтобы избежать непреднамеренного назначения
Вот несколько примеров некоторых интригующих лучших методов защитного программирования (примеры для конкретного языка есть в Java):
- Блокируйте свои переменные, пока не узнаете, что вам нужно их изменить
То есть вы можете объявить все переменные final
пока вы не знаете, что вам нужно будет изменить его, после чего вы можете удалить final
, Один обычно неизвестный факт заключается в том, что это также верно для параметров метода:
public void foo(final int arg) { /* Stuff Here */ }
- Когда происходит что-то плохое, оставьте след улик
Есть несколько вещей, которые вы можете сделать, когда у вас есть исключение: очевидно, зарегистрировать его и выполнить некоторую очистку будет несколько. Но вы также можете оставить след (например, установка переменных для значений часового типа, таких как "UNABLE TO LOAD FILE" или 99999, будет полезна в отладчике на случай, если вы пропустите исключение catch
-блок).
- Когда дело доходит до последовательности: дьявол кроется в деталях
Будьте максимально совместимы с другими библиотеками, которые вы используете. Например, в Java, если вы создаете метод, который извлекает диапазон значений, сделайте нижнюю границу включающей и верхнюю границу исключающей. Это сделает его совместимым с такими методами, как String.substring(start, end)
который работает таким же образом. Вы найдете все эти типы методов в Sun JDK, которые ведут себя таким образом, поскольку он делает различные операции, включая итерацию элементов, совместимой с массивами, где индексы имеют значение от нуля (включительно) до длины массива (исключая).
Так какие у тебя любимые защитные практики?
Обновление: если вы еще этого не сделали, не стесняйтесь, принимайте участие. Я даю шанс получить больше ответов, прежде чем я выберу официальный ответ.
67 ответов
В C++ мне когда-то нравилось переопределять новое, чтобы оно обеспечивало дополнительную память для перехвата ошибок забора.
В настоящее время я предпочитаю избегать защитного программирования в пользу Test Driven Development. Если вы обнаруживаете ошибки быстро и внешне, вам не нужно путать ваш код с защитными маневрами, ваш код - СУХОЙ, и вы получаете меньше ошибок, от которых вам нужно защищаться.
Избегайте защитного программирования, вместо этого используйте Fail Fast.
Под защитным программированием я подразумеваю привычку писать код, который пытается компенсировать какой-либо сбой в данных, писать код, предполагающий, что вызывающие могут предоставлять данные, которые не соответствуют контракту между вызывающей и подпрограммой, и что подпрограмма должна каким-то образом справляться. с этим.
SQL
Когда мне нужно удалить данные, я пишу
select *
--delete
From mytable
Where ...
Когда я запускаю его, я узнаю, забыл ли я или не испортил условие where. У меня есть безопасность. Если все в порядке, я выделяю все после маркеров комментариев "-" и запускаю его.
Изменить: если я удаляю много данных, я буду использовать счетчик (*) вместо просто *
Выделите разумный кусок памяти при запуске приложения - я думаю, что Стив Макконнелл назвал это " парашютом памяти" в Code Complete.
Это может быть использовано в случае, если что-то серьезное идет не так, и вы должны прекратить.
Выделение этой памяти заранее обеспечивает вам сеть безопасности, так как вы можете освободить ее и затем использовать доступную память для выполнения следующих действий:
- Сохраните все постоянные данные
- Закройте все соответствующие файлы
- Записать сообщения об ошибках в файл журнала
- Представить значимую ошибку пользователю
В каждом операторе switch, у которого нет регистра по умолчанию, я добавляю регистрацию, которая прерывает программу с сообщением об ошибке.
#define INVALID_SWITCH_VALUE 0
switch (x) {
case 1:
// ...
break;
case 2:
// ...
break;
case 3:
// ...
break;
default:
assert(INVALID_SWITCH_VALUE);
}
Когда вы обрабатываете различные состояния перечисления (C#):
enum AccountType
{
Savings,
Checking,
MoneyMarket
}
Тогда внутри какой-то рутины...
switch (accountType)
{
case AccountType.Checking:
// do something
case AccountType.Savings:
// do something else
case AccountType.MoneyMarket:
// do some other thing
default:
--> Debug.Fail("Invalid account type.");
}
В какой-то момент я добавлю еще один тип учетной записи к этому перечислению. И когда я это сделаю, я забуду исправить это заявление переключателя. Итак Debug.Fail
ужасно падает (в режиме отладки), чтобы привлечь мое внимание к этому факту. Когда я добавляю case AccountType.MyNewAccountType:
ужасный сбой прекращается... пока я не добавлю еще один тип учетной записи и не забуду обновить дела здесь.
(Да, полиморфизм здесь, наверное, лучше, но это всего лишь пример из головы.)
При распечатке сообщений об ошибках со строкой (в частности, которая зависит от ввода пользователя), я всегда использую одинарные кавычки ''
, Например:
FILE *fp = fopen(filename, "r");
if(fp == NULL) {
fprintf(stderr, "ERROR: Could not open file %s\n", filename);
return false;
}
Это отсутствие цитат вокруг %s
действительно плохо, потому что имя файла - пустая строка или просто пробел или что-то в этом роде. Распечатанное сообщение, конечно, будет:
ERROR: Could not open file
Итак, всегда лучше сделать:
fprintf(stderr, "ERROR: Could not open file '%s'\n", filename);
Тогда, по крайней мере, пользователь видит это:
ERROR: Could not open file ''
Я считаю, что это имеет огромное значение с точки зрения качества отчетов об ошибках, представляемых конечными пользователями. Если вместо чего-то общего звучит странное сообщение об ошибке, подобное этому, то они с большей вероятностью скопируют / вставят его, вместо того, чтобы просто написать "это не откроет мои файлы".
Безопасность SQL
Прежде чем писать какой-либо SQL, который будет изменять данные, я обертываю все это в откат транзакции:
BEGIN TRANSACTION
-- LOTS OF SCARY SQL HERE LIKE
-- DELETE FROM ORDER INNER JOIN SUBSCRIBER ON ORDER.SUBSCRIBER_ID = SUBSCRIBER.ID
ROLLBACK TRANSACTION
Это предотвращает выполнение неверного удаления / обновления навсегда. И вы можете выполнить все это и проверить разумное количество записей или добавить SELECT
заявления между вашим SQL и ROLLBACK TRANSACTION
чтобы убедиться, что все выглядит правильно.
Когда вы будете полностью уверены, что это делает то, что вы ожидали, измените ROLLBACK
в COMMIT
и беги по-настоящему.
Для всех языков:
Сократите область применения переменных до минимально возможных. Откажитесь от переменных, которые только что предоставлены, чтобы перенести их в следующую инструкцию. Переменные, которые не существуют, являются переменными, которые вам не нужно понимать, и вы не можете нести за них ответственность. Всегда используйте лямбды по той же причине.
Если сомневаетесь, бомбите приложение!
Проверяйте каждый параметр в начале каждого метода (независимо от того, явно ли вы его кодируете или используете контрактное программирование, здесь не имеет значения) и бомбите с правильным исключением и / или значимым сообщением об ошибке, если какое-либо предварительное условие для кода не встречал
Мы все знаем об этих неявных предварительных условиях, когда пишем код, но если они явно не проверены, мы создаем лабиринты для себя, когда позже что-то идет не так, и стеки из десятков вызовов методов отделяют возникновение симптома и фактическое местоположение где предварительное условие не выполнено (= где проблема / ошибка на самом деле).
В Java, особенно с коллекциями, используйте API, поэтому, если ваш метод возвращает тип List (например), попробуйте следующее:
public List<T> getList() {
return Collections.unmodifiableList(list);
}
Не позволяйте ничему сбежать из вашего класса, что вам не нужно!
В Perl все
use warnings;
мне нравится
use warnings FATAL => 'all';
Это приводит к смерти кода для любого предупреждения компилятора / среды выполнения. Это в основном полезно при отлове неинициализированных строк.
use warnings FATAL => 'all';
...
my $string = getStringVal(); # something bad happens; returns 'undef'
print $string . "\n"; # code dies here
C#:
string myString = null;
if (myString.Equals("someValue")) // NullReferenceException...
{
}
if ("someValue".Equals(myString)) // Just false...
{
}
В C# проверка string.IsNullOrEmpty перед выполнением каких-либо операций над строкой, таких как length, indexOf, mid и т. Д.
public void SomeMethod(string myString)
{
if(!string.IsNullOrEmpty(myString)) // same as myString != null && myString != string.Empty
{ // Also implies that myString.Length == 0
//Do something with string
}
}
[Редактировать]
Теперь я также могу сделать следующее в.NET 4.0, которая дополнительно проверяет, является ли значение пустым пространством
string.IsNullOrWhiteSpace(myString)
В Java и C# дайте каждому потоку осмысленное имя. Это включает потоки пула потоков. Это делает дампы стека намного более значимыми. Требуется немного больше усилий, чтобы дать осмысленное имя даже потокам пула потоков, но если у одного пула потоков есть проблема в долго работающем приложении, я могу вызвать дамп стека (вы знаете о SendSignal.exe, верно?), возьмите логи, и без прерывания работающей системы я могу сказать, какие потоки... что угодно. В тупик, протекает, растет, в чем проблема.
В VB.NET опции Explicit и Option Strict включены по умолчанию для всей Visual Studio.
С Java может быть удобно использовать ключевое слово assert, даже если вы запускаете производственный код с отключенными утверждениями:
private Object someHelperFunction(Object param)
{
assert param != null : "Param must be set by the client";
return blahBlah(param);
}
Даже с выключенными утверждениями, по крайней мере, код документирует тот факт, что параметр должен быть где-то установлен. Обратите внимание, что это частная вспомогательная функция, а не член открытого API. Этот метод может вызываться только вами, поэтому можно делать определенные предположения о том, как он будет использоваться. Для открытых методов, вероятно, лучше создать реальное исключение для неверного ввода.
C++
#define SAFE_DELETE(pPtr) { delete pPtr; pPtr = NULL; }
#define SAFE_DELETE_ARRAY(pPtr) { delete [] pPtr; pPtr = NULL }
затем замените все ваши вызовы ' delete pPtr ' и ' delete [] pPtr ' на SAFE_DELETE(pPtr) и SAFE_DELETE_ARRAY(pPtr)
Теперь по ошибке, если вы используете указатель "pPtr" после его удаления, вы получите ошибку "нарушение прав доступа". Это гораздо проще исправить, чем случайные повреждения памяти.
Я не нашел readonly
Ключевое слово, пока я не нашел ReSharper, но теперь я использую его инстинктивно, особенно для классов обслуживания.
readonly var prodSVC = new ProductService();
В Java, когда что-то происходит, и я не знаю почему, я иногда буду использовать Log4J так:
if (some bad condition) {
log.error("a bad thing happened", new Exception("Let's see how we got here"));
}
таким образом я получаю трассировку стека, показывающую, как я попал в неожиданную ситуацию, скажем, блокировка, которая никогда не разблокируется, что-то нулевое, которое не может быть нулевым, и так далее. Очевидно, что если выдается настоящее исключение, мне не нужно этого делать. Это когда мне нужно увидеть, что происходит в рабочем коде, не мешая ничего другого. Я не хочу бросать исключение, и я его не поймал. Я просто хочу, чтобы трассировка стека была зарегистрирована с соответствующим сообщением, чтобы пометить меня в том, что происходит.
Если вы используете Visual C++, используйте ключевое слово override всякий раз, когда вы переопределяете метод базового класса. Таким образом, если кто-нибудь когда-нибудь изменит сигнатуру базового класса, он выдаст ошибку компилятора, а не неверный метод, вызываемый без вывода сообщений. Это бы спасло меня несколько раз, если бы существовало раньше.
Пример:
class Foo
{
virtual void DoSomething();
}
class Bar: public Foo
{
void DoSomething() override { /* do something */ }
}
C#
- Проверьте ненулевые значения для параметров ссылочного типа в открытом методе.
- я использую
sealed
много для классов, чтобы избежать введения зависимостей там, где я их не хотел. Разрешение наследования должно быть сделано явно, а не случайно.
Я узнал в Java, что почти никогда не жду, пока разблокируется блокировка, если только я действительно не ожидаю, что это может занять бесконечно много времени. Если реально, блокировка должна разблокироваться в течение нескольких секунд, тогда я буду ждать только определенное время. Если блокировка не разблокируется, то я жалуюсь и сбрасываю стек в журналы, и в зависимости от того, что лучше для стабильности системы, либо продолжаю, как будто блокировка разблокирована, либо продолжаю, как будто блокировка никогда не разблокируется.
Это помогло выделить несколько условий гонки и псевдоблокировки, которые были таинственными, прежде чем я начал это делать.
Когда вы выдаете сообщение об ошибке, по крайней мере, попытайтесь предоставить ту же информацию, которая была у программы, когда она приняла решение выдать ошибку.
"Отказано в доступе" означает, что возникла проблема с разрешением, но вы не знаете, почему или где возникла проблема. "Невозможно записать журнал транзакций / мой / файл: файловая система, доступная только для чтения", по крайней мере, позволяет узнать основу, на которой было принято решение, даже если оно неправильное, особенно если оно неправильное: неправильное имя файла? открыл неправильно? другая неожиданная ошибка? - и дает вам знать, где вы были, когда у вас возникла проблема.
Джава
Java-API не имеет понятия неизменных объектов, что плохо! Финал может помочь вам в этом случае. Пометьте каждый класс, который является неизменным, финалом и подготовьте класс соответствующим образом.
Иногда полезно использовать final для локальных переменных, чтобы они никогда не меняли свое значение. Я нашел это полезным в некрасивых, но необходимых конструкциях цикла. Просто легко случайно использовать переменную, даже если она исправлена как константа.
Используйте защитное копирование в ваших добытчиках. Если вы не возвращаете примитивный тип или неизменный объект, убедитесь, что вы скопировали объект, чтобы не нарушать инкапсуляцию.
Никогда не используйте клон, используйте конструктор копирования.
Узнайте контракт между equals и hashCode. Это нарушается так часто. Проблема в том, что это не влияет на ваш код в 99% случаев. Люди перезаписывают равно, но не заботятся о hashCode. Существуют случаи, когда ваш код может сломаться или вести себя странно, например, использовать изменяемые объекты в качестве ключей на карте.
В C# используйте as
ключевое слово для приведения.
string a = (string)obj
сгенерирует исключение, если obj не является строкой
string a = obj as string
оставит как ноль, если объект не является строкой
Вам все еще нужно принять во внимание ноль, но это, как правило, более просто, чем поиск исключений приведения. Иногда вам нужно поведение типа "брось или взорви", и в этом случае (string)obj
синтаксис является предпочтительным.
В моем собственном коде я использую as
синтаксис около 75% времени, и (cast)
синтаксис около 25%.
Будьте готовы к любому вводу, и любой неожиданный вывод, дамп в журналы. (В пределах разумного. Если вы читаете пароли от пользователя, не сбрасывайте их в журналы! И не записывайте тысячи сообщений такого рода в журналы в секунду. Причины, связанные с содержимым, вероятностью и частотой, прежде чем регистрировать его.)
Я говорю не только о проверке пользовательского ввода. Например, если вы читаете HTTP-запросы, которые, как вы ожидаете, содержат XML, будьте готовы к другим форматам данных. Я был удивлен, увидев ответы HTML, где я ожидал только XML - пока я не посмотрел и не увидел, что мой запрос проходил через прозрачный прокси-сервер, о котором я не знал, и что клиент заявил о незнании - и время ожидания прокси-сервера истекло при попытке завершить запрос. Таким образом, прокси-сервер возвратил страницу с ошибкой HTML моему клиенту, сбив с толку клиента, который ожидал только данные XML.
Таким образом, даже если вам кажется, что вы контролируете оба конца провода, вы можете получить неожиданные форматы данных без какого-либо злодейства. Будьте готовы, защитите код и предоставьте диагностический вывод в случае неожиданного ввода.
Я стараюсь использовать подход "Дизайн по контракту". Это может быть эмулировано во время выполнения на любом языке. Каждый язык поддерживает "assert", но проще и удобнее написать лучшую реализацию, которая позволит вам более эффективно управлять ошибкой.
В " 25 самых опасных ошибках программирования" "Неправильная проверка ввода" - самая опасная ошибка в разделе "Небезопасное взаимодействие между компонентами".
Добавление предварительных условий в начале методов - хороший способ убедиться, что параметры согласованы. В конце методов я пишу постусловия, которые проверяют, что выходные данные - это то, что и должно быть.
Чтобы реализовать инварианты, я пишу метод в любом классе, который проверяет "согласованность классов", который должен вызываться автоматически макросом предусловия и постусловия.
Я оцениваю библиотеку контрактов.
Я забыл написать echo
в PHP слишком много раз:
<td><?php $foo->bar->baz(); ?></td>
<!-- should have been -->
<td><?php echo $foo->bar->baz(); ?></td>
Мне потребовалось бы целую вечность, чтобы попытаться выяснить, почему ->baz() ничего не возвращал, хотя на самом деле я просто не повторял это! Итак, я сделал EchoMe
класс, который может быть обернут вокруг любого значения, которое должно отображаться:
<?php
class EchoMe {
private $str;
private $printed = false;
function __construct($value) {
$this->str = strval($value);
}
function __toString() {
$this->printed = true;
return $this->str;
}
function __destruct() {
if($this->printed !== true)
throw new Exception("String '$this->str' was never printed");
}
}
А затем для среды разработки я использовал EchoMe, чтобы обернуть вещи, которые должны быть напечатаны:
function baz() {
$value = [...calculations...]
if(DEBUG)
return EchoMe($value);
return $value;
}
Используя эту технику, в первом примере отсутствует echo
сейчас бы скинул исключение...
Используйте систему ведения журналов, которая позволяет динамически настраивать уровень журнала во время выполнения. Часто, если вам нужно остановить программу, чтобы включить ведение журнала, вы потеряете любое редкое состояние, в котором произошла ошибка. Вы должны иметь возможность включить дополнительную информацию о ведении журнала, не останавливая процесс.
Кроме того, 'strace -p [pid]' в linux покажет, что вы хотите, чтобы системные вызовы выполнял процесс (или поток linux). Поначалу это может показаться странным, но как только вы привыкнете к тому, какие системные вызовы обычно выполняются с помощью вызовов libc, вы обнаружите, что это неоценимо для диагностики в полевых условиях.
Всегда компилируйте на самом высоком уровне предупреждений и обрабатывайте предупреждения как ошибки (сборщики прерываний).
Даже если код "правильный", исправьте причину предупреждения, не отключая предупреждение, если это вообще возможно. Например, ваш компилятор C++ может дать вам предупреждение для легального кода, например:
while (ch = GetNextChar()) { ... }
Похоже, вы могли набрать =
вместо ==
, Большинство компиляторов, которые предлагают это (полезное) предупреждение, закроются, если вы добавите явную проверку.
while ((ch = GetNextChar()) != 0) { ... }
Быть немного более явным не только заставляет замолчать предупреждение, но и помогает следующему программисту, который должен понимать код.
Если вы ДОЛЖНЫ отключить предупреждение, используйте #pragma
в коде, таким образом, вы можете (1) ограничить диапазон кода, для которого отключено предупреждение, и (2) использовать комментарии, чтобы объяснить, почему предупреждение должно быть отключено. Предупреждения, отключенные в командных строках или make-файлах, - это катастрофы, ожидающие своего появления.