Опасный код на C

Перевод цикла статей David Chisnall «Writing Insecure C, Part 1», «Writing Insecure C, Part 2», «Writing Insecure C, Part 3».

Использование языка программирования C часто приводит к написанию очень опасного кода. Но это не совсем справедливое обвинение; такие проекты, как OpenBSD показывают, что возможно писать безопасный код на C. Проблема C та же, что и в ассемблере — язык открывает вам все возможности архитектуры, но и кое-что ещё. Он дает все возможности для написания безопасного кода, но не делает эти вещи сам.

В этой статье рассмотрены стандартные примеры ошибок в коде C и то, как их и избежать.

Проверка ошибок

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

В частности, код на Java часто засорен блоками try…catch, которые ничего не делают, кроме отбрасывания ошибок, но даже в этом случае механизм исключений преследует цель: это заставляет программиста помнить о том, что он небезопасно обрабатывает условия ошибок.

В C большинство функций возвращают неправильное значение, когда произошла какая-либо ошибка. Обычно это делается двумя способами. Множество функций возвращают код ошибки ноль в случае успеха. Функции, возвращающие указатели, возвращают правильное значение в случае успеха или ноль в противном случае. Эта ситуация может немного смущать, так как ноль означает и успешное выполнение одних функций, и ошибку в других.

Можно возвращать нулевой указатель. Это вы не сможете легко проигнорировать, так как получите ошибку сегментации, как только попытаетесь разыменовать его. Этот подход действительно опасен только в тех функциях, которые почти никогда не завершаются с ошибкой; в других ошибка может быть обнаружена во время тестирования и исправлена.

Стандартный пример функции, которая почти никогда не завершается с ошибкой — malloc(), наряду со связанными с ней функциями, вроде calloc(). В спецификации C указано, что malloc должна вернуть NULL в случае, если памяти недостаточно, чтобы удовлетворить запрос. Linux не полностью следует этому правилу: он возвращает NULL, если в системе нет достаточного виртуального пространства адресов, чтобы выполнить выделение памяти, но в случае недостатка памяти, Linux по-прежнему выделяет пространство адресов, а затем возвращает ошибку, если вы пытаетесь использовать эту память. Как бы то ни было, исходя из того, что у вас есть подходящая реализация C, необходимо проверять значения, которые возвращает malloc.

В большинстве случаев, вы не сможете сделать ничего разумного, если malloc завершится с ошибкой. Даже код, восстанавливающий ошибку, обычно нуждается в выделении памяти. Вы можете попробовать выделять эту память, когда программа запускается (не забудьте проверить, что вы можете получать к ней доступ). В качестве альтернативы, вы можете использовать что-нибудь вроде этого макроса:

#define MALLOC(x,y) do { y = malloc(x); if (!y) abort(1); } while(0)

Он будет тестировать каждое выделение памяти, и прерывать программу в случае ошибки. Вы можете заменить вызов abort на ваш код, обрабатывающий ошибку, но будьте осторожны. Одна из недавних уязвимостей в OpenSSH была вызвана вызовом кода, восстанавливающего ошибку в ситуации, когда программа была в неопределенном состоянии. Зачастую, прерывание выполнения безопаснее.

Точно также важна проверка возвращаемых значений других функций.

Начальные значения

Если вы объявляете глобальную переменную в C, она безоговорочно инициализируется нулем. Этот способ очень удобный, и был бы ещё более удобным, если бы вы могли верить, что автор вашего компилятора прочитал эту часть спецификации. Как бы то ни было, если вы получаете память из любого другого источника, это правило не работает.

При объявлении локальной переменной, компилятор выполняет это как простой инкремент верхнего регистра стека. Начальное значение переменной будет таким, каким последняя функция его установила, использовав эту часть памяти стека.

Здесь существуют две потенциальные проблемы. Первая в том, что невозможно предсказать поведение неинициализированной переменной. Вторая в том, что этого может и не быть. В общем, использование значения перед инициализацией приведет к неопределенному состоянию вашей программы. В худшем случае, это может привести к утечке информации. Например, если вы пишете некоторый код, который использует неинициализированную переменную после вызова функции шифрования, ваше «случайное» значение неопределенной переменной может содержать некоторую часть ключа шифрования. Если ваш код запускает скрипты, которые могут читать информацию из стека, это может быть серьезной уязвимостью.

Ещё несколько проблем могут случиться с неинициализированными переменными. Одна из самых ужасных, что я видел, это когда вы начинаете с кода, который выглядит как-то так:

int a = 42;

А затем вы решаете, что вам необходимо условие для инициализации, поэтому вы копируете и вставляете его в выражение if:

if ({некоторое условие})
 
{
 
   int a = 42;
 
}

Ой, вы же забыли объявить переменную за пределами блока if, поэтому вы делаете это позже, и даете ей значение по умолчанию:

int a = 0;
 
if ({некоторое условие})
 
{
 
   int a = 42;
 
}

Теперь у вас есть код, который компилируется и запускается. Большинство времени (когда не встречается условие), он будет работать. Но код внутри фигурных скобок будет «холостым». Он определяет новую переменную под названием a и присваивает ей 42. Как только блок закончится, эта переменная выйдет из области видимости и останется старая a, до сих пор со значением 0.

Более незаметный вариант может возникнуть при опечатке в инициализации, когда вы выполняете что-то вроде этого:

int value = value + 12;

когда на самом деле ожидаете выполнение этого:

int value = othervalue + 12;

Как только компилятор распарсит int value, переменная станет валидной и будет находиться в области видимости. Вы можете считать её значение, добавить 12 к нему и присвоить результат ей же. К несчастью для вас, value, которую вы считываете, не определена. Теперь вам придется присвоить value неопределенное значение. Если вы считываете неосторожно, вы можете подумать, что проинициализировали её, хотя вы этого и не сделали. Компилятор с оптимизацией удалит +12, поскольку неопределенность плюс 12 есть неопределенность, и это будет эквивалентно следующему:

int value;

Если в вашем компиляторе есть выдача предупреждений о том, что не было проинициализировано, он должен выявить эти проблемы. К сожалению, выключение этих предупреждений происходит довольно часто, потому как в C нет способа указать выходные параметры. Вот почему следующие вещи встречаются относительно часто:

int a;
 
call_some_function(&a);

Это может и не быть ошибкой, если a используется для возвращения некоторого дополнительного значения из функции. В других случаях, функция будет считывать значение a, которая неопределенно. В компиляторе C нет способа узнать, правильно ли используется a, поэтому она может использоваться, будучи неопределенной.

Проблемы целых

Если вы используете высокоуровневые языки программирования, поддержка числовых переменных в C может показаться мучительно примитивной. То же самое может быть, если раньше вы писали на ассемблере, так как C также не открывает программисту условных кодов современных ЦПУ.

В высокоуровневых языках, числа обычно условно-точные, причем точность определена так, как вам надо. В C есть набор целых типов. Наиболее часто используемый — это int, который должен соответствовать машинному слову. На компьютерах, на которых я учил C, оно было длиной в 16 бит. Теперь он обычно размером в 32 бита, даже на архитектурах, где машинное слово имеет длину 64 бита, так как большое количество написанного кода подразумевает, что он всегда имеет длину 32 бита.

Одна из наиболее частых причин странного поведения — попытка хранить значение указателя в int. На обычных 32-битных платформах, этот метод работает хорошо. На некоторых 64-битных он работает, а на некоторых — нет. Стандарт C99 определяет новый целый тип, intptr_t, который гарантированно имеет достаточный размер, чтобы хранить указатель. Если вы планируете хранить указатель в int, вы всегда должны использовать intptr_t.

Указатель — один из других слегка сбивающих с толку моментов. C определяет два типа указателей: один указывает на код, другой указывает на данные. Указатель на функцию не имеет гарантировано такой же размер, что и указатель, указывающий на данные. На множестве встроенных платформ, часто используются 16-битные указатели для кода и 32-битные для данных. Преобразование типа в void* указателя на функцию приведет к тому, что некоторые данные будут отброшены.

Другим основным видом целого в C является char, short и long. В C99 также определен long long. Ни один из них не имеет фиксированного размера, но все они имеют минимальные размеры (технически у них есть минимальный набор значений, который они могут хранить, не предоставляя гарантий по внутреннему формату). Short должен быть, по крайней мере, 16 бит, long как минимум 32, а lon long как минимум 64. Если вам необходимо минимальное количество точности, выберите один из них, и вы сможете получить дополнительное пространство к тому, что запросили, в зависимости от архитектуры.

Я не упомянул char ещё и потому, что они детально отличаются от других типов. Все другие основные целые типы данных являются знаковыми, пока не будут объявлены, как беззнаковые. Это не всегда так в случае с char. К сожалению, в случае с char, знаковая она, или нет, полностью зависит от реализации. Если вы используете char, всегда явным образом объявляйте их как знаковые или нет, так как в противном случае, вы можете быть удивлены позднее.

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

 a = b + c;

Так как вы храните результат в a, вы можете предположить, что сложение будет выполнено, каким бы типом не была a. По факту, оно будет выполнено с типом b и c. Это имеет смысл тогда, когда вы думаете, что лучше иметь значение (b + c) в зависимости от чего-то, чем b и c сами по себе. Вы можете думать, что тип этого выражения будет типом b. Стандарт C99 определяет набор правил для определения типа, но в основном он будет представлять собой тот тип, который больше, b или c. Например, если a — char, а b — int, тип выражения будет int. Поразительно часто встречается ошибка, когда делают что-то вроде такого:

long long a;
 
long b;// = чему-то
 
long c;// = чему-то
 
a = b * c;

a как минимум 64 бита, b и c как минимум 32 бита. Так как у вас есть только гарантия того, что b и c по 32 бита, вы не должны помещать нечто большее в них, даже если ваш компилятор или платформа имеет long в 64 бита. Когда вы умножаете их, вы получаете результат, подходящий для 64-битного int и вы храните его в чем-то, что подходит для 64-битного int. Звучит удобно? Так и есть, кроме того факта, что результат искажается до 32 бит прямо перед присваиванием. Вот правильный способ сделать это:

a = (long long)b * c;

Это расширяет b до 64 бит (или больше, в зависимости от реализации). Расширение типа гарантирует, что c имеет тип такого же размера, что и b, так что она также расширена. Тогда умножение происходит как умножение двух long long с 32 или более первыми нулями, а результат (long long) хранится в переменной типа long long.

В основном, вы можете избежать сюрпризов путем прямого преобразования типов к чему-то с достаточной точностью перед выполнением операции. Убедитесь, что отсутствие знака выполняется для всех переменных. Это очень частая причина ошибок, так как вы неожиданно теряете первый бит при преобразовании большого беззнакового значения к его знаковому эквиваленту.

Более неуловимая ошибка возникает от переполнения int. Она особенна часто возникает при использовании malloc, так как стандартный шаблон написания — malloc(i * sizeof(x)). Если у взломщика есть влияние на i, он может попытаться выполнить это переполнение. Для очень больших значений i, это даст результат гораздо меньший, чем i, что приведет к проблеме. Вызов malloc будет успешным, но когда вы попытаетесь использовать массив, полученный в результате, только первые несколько значений будут валидными. Взломщик может вынудить вас переписать другие данные.

Простым путем избегания этого вида уязвимости может быть использование calloc() вместо malloc() (конечно, в надежде, что реализация calloc в вашей системе производит проверку границ сама, а не просто делает malloc() и memset() для количество*размер байт).

realloc() — ещё одна проблема. Нет стандартного пути сделать это с ней, поэтому вам надо делать это самому. К счастью, OpenSSH включает в себя xrealloc(), который является версией realloc() с проверкой границ. Она включает несколько других проверок, но если вам не нужны все из них, вы можете реализовать упрощенную версию:

void * xrealloc(void *ptr, size_t nmemb, size_t size)
 
{
 
    void *new_ptr;
 
    size_t new_size = nmemb * size;
 
    if (SIZE_T_MAX / nmemb < size)
 
            return NULL;
 
        return realloc(ptr, new_size);
 
}

Этот тест довольно просто. SIZE_T_MAX — это максимальное значение, которое может иметь size_t. Когда вы делите на указанное количество элементов, вы получаете максимальный размер, который может быть без переполнения. Если этот размер меньше, чем требуемое пространство, возникает переполнение, поэтому вы возвращаете NULL.

realloc возвращает NULL в случае ошибки, так что вам всегда следует проверять возвращаемое значение realloc на валидность. К сожалению, это является наиболее частой причиной утечек памяти (которые, в свою очередь, являются причиной атак DDoS). Если realloc() возвращает NULL, исходный указатель по-прежнему является валидным. Часто разработчики забывают этот принцип и просто делают что-то вроде этого:

ptr = realloc(ptr, newsize);

Когда realloc() возвращает NULL, вы теряете вашу ссылку на предыдущее выделение памяти. FreeBSD предоставляет удобную функцию, reallocf(), которая эквивалентна следующей:

void *reallocf(void* ptr, size_t size)
 
{
 
    void *newptr = realloc(ptr, size);
 
    if (NULL == newptr)
 
    {
 
        free(ptr);
 
    }
 
    return newptr;
 
}

Если у вас нет кода для обработки случая, когда realloc() завершается с ошибкой, вам необходимо использовать что-то вроде неё.

Проблемы с памятью

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

Эта методика приводит к проблемам при передаче данных назад к вызываемой функции. Для структур очевидным решением является просто вернуть структуру. Когда она скомпилирована, вызывающая функция может выделить структуру и передать указатель к вызываемой функции, которая копирует данные в свое пространство. Тем не менее, это приводит к большому количеству копий.

Что ещё хуже, это не работает для всех динамически выделяемых данных. Рассмотрим что-нибудь, вроде sprintf(). Этот аналог printf() пишет в буфер вместо стандартного вывода. Проблема sprintf в том, что вызывающей функции необходимо знать размер буфера, что не всегда просто — на самом деле, это требует реализации большинства кода sprintf в вызывающей функции.

По факту, почти невозможно использовать sprintf безопасно. Вам необходимо указать длину каждого отдельного элемента строки формата (вот почему sprintf была создана). Это может привести к завершению процесса (что может также вызвать проблемы безопасности, и мы вернемся к этому позднее), поэтому некоторые реализации libc включают в себя asprintf().

Функция asprintf() — это то же самое, что и sprintf, кроме того, что она выделяет свой собственный буфер с помощью malloc. Это позволяет избежать досрочного завершения процесса или переполнения. К несчастью, она приносит новую проблему: когда вызывающая функция должна освобождать указатели, которые возвращает вызываемая функция?

Большое количество кода на C содержит в себе эту проблему. Решение обычно заключается в том, чтобы поместить в документацию о функции строку «вызывающая функция должна освободить возвращаемый указатель». К сожалению, этот подход делает сложной возможность взглянуть на часть кода и увидеть, валидна ли она. Одно из решений заключается в том, чтобы заключать в оболочку каждый указатель, например, так:

typedef struct _RefCountedBuffer
 
{
 
    void *buffer;
 
    int refcount;
 
    void (free*)(struct _RefCountedBuffer*);
 
} *RefCountedBuffer;

Когда вы возвращаете указатель, вы создаете одну из этих структур и устанавливаете refcount в 1. Когда вы получаете его, вы всегда вызываете функцию, которая увеличивает refcount на единицу и вызывает соответствующую функцию free(), если она достигнет нуля.

Эта методика близка для программистов Objective-C, потому как OpenStep реализует схожий механизм работы. GNUstep продвинулся дальше, предоставляя макросы ASSIGN() и DESTROY(). Эти макросы делают ошибки при работе с памятью более редкими, и мы можем сделать нечто похожее на обычном C.

Прежде всего, нам надо определить функции retain() и release():

RefCountedBuffer retain(RefCountedBuffer *buf)
 
{
 
    buffer->refcount++;
 
    return buffer;
 
}
 
void release(RefCountedBuffer *buf)
 
{
 
    buf->refcount--;
 
    if (buf->refcount == 0)
 
        buf->free(buf);
 
}

Учтите, что это упрощенные версии тех функций, что вам могут действительно потребоваться. Наиболее распространенная проблема — они не являются потоко-безопасными. Операторы ++ и — обычно компилируются в последовательность команд загрузить, добавить (или вычесть), сохранить. Если два потока, например, одновременно аккумулируют, они оба должны сначала загрузить эти значения, прежде чем сохранить их, и одна аккумуляция будет потеряна. Вы можете обойти эту проблему, используя специальный ассемблер для ЦПУ или встроенные средства GCC для элементарных операций.

Определив эти функции, вы можете определить макросы SET и FREE следующим образом:

#define SET(var, val) do { 
 
RefCountedBuffer __tmp = retain(val); 
 
if (NULL != var) release(var) var = __tmp; } while(0)

Учтите, что вы удерживаете новое значение перед освобождением старого, что может привести к проблемам в случае, если новое и старое значения равны. Соответствующий макрос FREE() довольно прост:

#define FREE(var) do { release(var) var = NULL; } while(0)

Этот макрос гарантирует, что каждый указатель всегда установлен в NULL после его освобождения. Даже если вы не используете подсчет ссылок, этот метод дает результат.

Если вы используете оба этих макроса, у вас будет очень мало знаков равенства в коде. Что делает проще просмотреть код и найти места, где могут быть ошибки, связанные с памятью.

Подсчет ссылок — хорошее решение для данных, предназначенных только для чтения. Вы можете вернуть некоторый внутренний компонент большой структуры данных в качестве ссылки, не удаляя его из оригинальной структуры, пока его не перестанут использовать — так долго, как он находится в структуре RefCountedBuffer в обоих случаях.

Тем не менее, эта модель не решает нашу первоначальную проблему функций, похожих на asprintf. Она требует вернуть строку, что часто используется только в вызывающей функции. Для этого выделение памяти в куче и заключения её в структуру подсчета ссылок будет лишним. Вам нужен способ выделить пространство в стековом фрейме вызывающей функции.

У разработчиков сервера Dovecot IMAP есть превосходное решение. В дополнение к обычному контролю стека, они делают отдельный стек данных и аналог asprintf, которая использует его. Вызывающая функция сначала вызывает функцию, которая выделяет фрейм в новом стеке данных. Затем она вызывает аналог asprintf(), которая выделяет пространство в стеке результатов. Этот подход работает правильно, пока вызывающая функция достает верхний фрейм стека данных.

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

Вам ничего не мешает иметь несколько независимых областей памяти. Вы можете использовать mmap() на /dev/zero, чтобы выделить где-либо непрерывный BLOB памяти и использовать его по своему желанию. Одной из возможных идей может быть выделение стека данных, который работает на такой же скорости, как и контрольный стек. Используйте этот метод для всех массивов, которые вы будете по-другому выделять в стеке. В отличие от контрольного стека, это может расти вверх в памяти. Вы можете сделать его переместимым, постоянно адресуя его через глобальный указатель к началу. Например, с помощью подобного макроса:

#define NEW_ARRAY(type, name, elements) 
 
__attribute__((cleanup(popDataStack)))    
 
type *name = dataStackCalloc(sizeof(type), elements)

__attribute__(cleanup) — это расширение GCC. Оно вызывает функцию popDataStack() с указателем на переменную в качестве аргумента, когда переменная выходит из области видимости. Теперь у вас есть указатель на что-то в стеке данных. Вместо использования прямого адреса, вы можете использовать макрос, который добавляет этот адрес указателю. Все это позволяет продолжать расти вашему стеку данных до тех пор, пока у вас есть достаточное количество непрерывной свободной памяти для хранения.

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

Буферы и строки

Строки в C — вечная причина проблем. Когда C был создан, было две концепции о том, как лучше всего реализовывать строки — известные сейчас как строки C и строки Pascal, в соответствии с языками, которые сделали эти идеи популярными. Такие языки, как Lisp использовали третью реализацию: строки являлись связанным списком символов (Erlang до сих пор использует эту модель).

Строки в стиле Lisp имеют очевидный недостаток. Каждому символу требуется один байт для хранения символа и 4 или 8 байт для хранения адреса следующего, — до девяти байт уходит на хранение одного байта данных. Эта структура далека от идеальной, но делает разделение и конкатенацию строк очень простой.

Более совершенные модели представляют строки, как связанный список массивов символов, позволяя легко их объединять.

Все эти модели могут быть (и это было сделано) реализованы на C, но стандартные строковые функции до сих пор работают с массивами байтов.

Большинство «классических» строковых функций практически невозможно использовать безопасно (по этой причине линковщик OpenBSD легко выдает предупреждение, когда вы используете одну из них). Каноническим примером «плохой» функции является strcat(), которая принимает два указателя на строки C. Функция проходит по первой строке, пока не найдет null; она записывает туда байты из второй строки пока не дойдет до null во второй строке. Вызывающая функция должна быть уверена, что существует достаточно места в первой строке, чтобы сохранить вторую.

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

В OpenBSD представлена strlcat, которая похожа на strncat, но возвращает сумму входных строк. Если результат работы функции больше третьего аргумента, имело место искажение. Эта функция находится в каждой ОС семейства BSD (включая Darwin/OS X), но её нет в glibc, так как, согласно главному разработчику glibc, является «бесполезным хламом BSD». К счастью, лицензия BSD позволяет вам копировать эту функцию из libc BSD в ваш код без каких-либо проблем.

Проблемы со строками в C велики из-за того факта, что строки — это просто массивы без проверки границ. Точно также, большинство проблем, затрагивающих строки, касаются и произвольных буферов.

Одной из самых опасных вещей в C99 является модель массивов переменной длины, которая позволяет вам выделять маленькие, с динамическим размером массивы в стеке. Вы можете всегда делать это с помощью alloca(), хотя качество реализаций alloca() варьируется между разными платформами. Следующие примерно эквивалентны:

int *a = alloca(sizeof(int) * n);
 
int a[n];

Разница в поведении, если существует недостаточно пространства для роста стека до n целых значений. Первая строка будет при этом NULL, но вы сможете проверить это, что упрощает отладку — достаточно просто обратиться к началу массива. Во второй строке, если не будет хватать размера стека, он будет указывать… куда-то. А куда именно, будет зависеть от реализации. Вот почему, если вы используете массивы C99 с переменной длиной, невозможно протестировать программу на переполнение стека. В большинстве случаев, это не является проблемой. Небольшие выделения памяти более безопасны для работы, но если взломщик сможет увеличить размер n, у вас может получиться массив, указывающий никуда.

Это является серьезной проблемой в зависимости от того, как реализован стек. В основном, нижняя часть стека является верхней границей памяти процессов, и она растет вниз. Если у вас есть массив в стеке, и вы выходите за его границы, вы перезаписываете стековый фрейм вызывающей функции. И, что ещё хуже, также перезаписываете возвращаемый адрес. Если вы используете что-то вроде strcat() с результирующей строкой в стеке, очень легко перезаписать возвращаемый адрес, позволяя взломщику контролировать, где происходит переход к выполнению, после того, как функция вернула результат.

Масштаб данной проблемы уменьшен в современных операционных системах (они проверяют целостность возвращаемого адреса и завершают процесс, если он невалиден), но это по-прежнему плохое решение. Сбои в программе лучше, чем удаленные эксплоиты, но нет ничего лучше валидного кода.

Когда все идет не так

В независимости от того, как тяжелы ваши усилия, у вас до сих пор остаются баги в коде. Разработчики OpenBSD говорят, что единственное отличие между багом и эксплойтом — интеллект взломщика, и это по большому счету, правда.

Ключом к безопасному программированию каждой части вашей программы является представление, что другая часть была написана идиотом. Вы можете проверить, что каждый входящий указатель — не NULL (хотя печально, что C не предоставляет какого-либо механизма для проверки, что он указывает на валидную часть памяти). Вы можете проверить, что любые другие значение находятся в ожидаемых вами рамках. Тем не менее, в конце концов, баг в одной части процесса может разрушить данные — а во многих операционных системах, даже код — всей остальной части процесса.

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

Разбивая компоненты программы на отдельные процессы, вы ограничиваете размеры разрушений, которые может принести один баг. Самым распространенным примером является разделение привилегий кода, находящееся во множестве серверных программ. Многие сервера должны быть запущены под root, или с соответствующими правами. Им нужна эта возможность для привязки к соответствующим портам и осуществления других действий в качестве root — таких, как получения доступа к данным разных пользователей или запуск программы под другим пользователем.

Хорошим методом для такого вида процессов является заключения кода, осуществляющего привилегированные операции, в отдельный процесс. Например, если вам надо написать на разные почтовые ящики пользователей, у вас может быть процесс, который запускается под root, чтобы открыть почтовый ящик, написать в него письмо, и больше ничего. Он проверит аргументы и осуществит запись, но не более того. Это легко реализовать в коде и довольно просто найти возможные баги.

Также можно изолировать код, содержащий важную информацию. Большей части программы не надо знать, например, ключей шифрования или паролей. Вы можете хранить такую информацию в отдельном процессе и ограничивать доступ главной программы к нему, усложнив тем самым для взломщика получение доступа к этим данным.

Отказ от привилегий

Хотя разделение привилегий работает хорошо, многое может быть сделано благодаря простому отказу от привилегий. UNIX содержит семейство системных вызовов setuid(), которые позволяют процессу, запущенному в качестве root, работать как другой пользователь.

Веб-сервер нуждается в запуске в качестве root, так как ему нужно быть привязанным к 80 порту и иметь доступ к файлам в директории public_html каждого пользователя. Тем не менее, как только он привязан к 80 порту, ему не нужно более работать как root, и он может отказаться от прав root. Хотя ему по-прежнему нужен способ получать доступ к директории public_html каждого пользователя. Одно решение — заставить каждого пользователя сделать его файлы доступными для группы веб-сервера. Другим может быть выполнение fork() процесса-потомка для каждого пользователя, который работает в качестве этого пользователя и имеет доступ к файлам в директории пользователя.

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

Пользователь с правами root может легко избежать chroot, создав новое устройство для жесткого диска и смонтировав его внутрь chroot (или даже получая доступ к нему напрямую). Вот почему важно сбросить привилегии root сразу же после входа в chroot.

Ещё легче использовать chroot(), если он встроен в приложение. Команда chroot, которая запускает процесс в окружении chroot, также доступна, но этот подход имеет две проблемы. Первая — он вызывает chroot до запуска процесса, поэтому программа и все необходимые библиотеки должны быть внутри директории chroot. Вторая заключается в том, что он должен выполняться в качестве root, поэтому ему нужно что-то внутри chroot для возможности сброса привилегий. Обычным решением является поместить команду su внутрь chroot. Хотя, когда вы поместите так много кода внутрь chroot, он станет выглядеть, как и внешнее окружение.

Разгоняя ядро

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

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

1. Пользовательский процесс запрашивает системный вызов.

2. Перехватывающий фреймворк проверяет валидность аргументов и решает, какой уровень привелегий должен быть у вызова (и должен ли он вообще выполняться).

3. Ядро обрабатывает вызов.

К сожалению, этот подход имеет немного неожиданное поведение. Многие системные вызовы принимают указатели в качестве аргументов. Обычно ядро отображается в адресное пространство каждого процесса (но в режиме привилегий помечается только как для чтения), поэтому обработчик системного вызова в ядре может получить доступ к тому, на что указывают эти указатели дешево (без копирования). Даже если он не может получить к ним доступ напрямую на платформах, где ядро имеет полностью отдельное адресное пространство, обычно оно так же может получать доступ к адресному пространству процесса дешево. Если вы производите системный вызов с указателем, вот добавочный шаг 2a:

2a. Другой поток изменяет данные, на которые указывает аргумент-указатель.

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

Такая же проблема возможна в коде пространства пользователя, использующего разделение привилегий, если он использует совместно используемую память. Простейшим решением будет всегда копировать текущую область памяти в привилегированный процесс перед тем, как обработать её. Эта методика хороша для небольших объемов данных, но не идеальна для более крупных. К сожалению, не существует хорошего решения этой проблемы, кроме отказа от использования совместно используемой памяти, но это обычно приводит к замедлению работы. Даже таким вещам, как конвейер требуется копирование данных в общий буфер, а затем копирование их назад из буфера. В будущем операционные системы, возможно, будут включать нечто между конвейером и буфером совместно используемой памяти, когда буфер находится в адресном пространстве получателя, но запись может быть осуществлена только ядром и разрешена, только когда получатель сообщает о том, что пространство доступно. Хотя, не похоже, что это будет осуществлено в скором времени.

Заключение

Писать безопасный код на C сложно, но это возможно. Безопасность таких систем, как OpenBSD доказывает, что это можно сделать. Язык не делает написание безопасного кода простым, но в некоторых случаях этот факт является полезным. Чтобы избежать проблем, программист должен основываться на хорошем коде, а не на возможностях языка.

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

Помимо этого решения, лучшими методами будут проверять входные значения в каждом случае, даже от источников, которым вы доверяете (вы не знаете, были ли они компрометированы и пытаются ли выполнить атаку изменения привилегий) и предпочитать аварийное завершение программы, чем попытки работать в неопределенном состоянии.

Регулярные выражения в JavaScript

Если вам нужно использовать регулярные выражения при создании сайта, вовсе не обязательно прибегать к использованию Perl или PHP, — JavaScript вполне может справится с этой работой. В этой статье мы рассмотрим объект RegExp в JavaScript и его методы. Я уверен, что после прочтения, JaveScript представится вам в другом свете.

Настоящий Мир

Большинство Настоящих Программистов относятся к JavaScript, словно к бедной родственнице из деревни — иногда полезна, но не очень важна. Настоящих Программистов не интересует язык, чье предназначение в том, чтобы заменять одно изображение другим или рисовать след за мышью по веб-странице. У Настоящих Программистов есть более важные дела.

Что ж, JavaScript ограничен стороной клиента в веб-транзакции, но на этой территории, он безусловный король. Нет языка, настолько простого к изучению или дающего возможность сделать столько полезных вещей с минимальными усилиями. И одно из того, что вы можете делать, это использовать регулярные выражения.

По ходу этой статьи, я собираюсь дать вам представление о регулярных выражениях в контексте JavaScript. Я покажу, как использовать объект String в JavaScript для простейших возможностей поиска соответствия или осуществления замены, так же, как и для более сложных действий со строками. И я представлю вам объект RegExp, который предоставляет удобный путь для создания более эффективного кода распространенной проверки входящих данных на стороне клиента. Итак, приступим; независимо от того, Настоящий ли вы Программист, или только пытаетесь им стать, я уверен, что вы найдете для себя кое-что полезное ниже.

Входим в Матрицу

Регулярные выражения, также известные как «регэкспы» среди программистов — мощный инструмент, использующийся для поиска по шаблону и замены подстрок. Они тесно связаны с почти всеми основанными на *nix утилитами, включая редакторы, вроде vi, скриптовые языки вроде Perl или PHP и консольными программами, такими, как awk и sed.

Регулярные выражения дают вам возможность построить шаблоны, используя набор специальных символов; эти шаблоны затем могут сопоставляться с текстом из файла, введенной в приложение датой или данными из формы, заполненной пользователем на сайте. В зависимости от того, есть ли совпадение, или нет, принимается соответствующее действие и вызывается соответственный программный код.

Например, одно из самых распространенных приложений регулярных выражений — проверка того, верен или нет введенным в форму пользователем адрес электронной почты; если да, форма будет принята, а если нет, появится сообщение, просящее пользователя исправить ошибку. Здесь регулярные выражения играют большую роль в процедурах интернет-приложений, принимающих решение, хотя, как вы увидите позже, они также могут иметь огромный эффект в сложных операциях поиска и замены.

Регулярное выражение обычно имеет вид, подобный этому:

/matrix/

Нахождение шаблона «matrix» это все, что оно делает. Как насчет чего-то более сложного? Попробуйте это:

/mat+/

Это найдет слова «matting» и «mattres», но не «matrix». Почему? Потому что символ «+» используется для вхождения одного или более символа, расположенного перед ним. В нашем случае, за символами «ma» следует одно или более вхождение буквы «t».

Также есть мета-символы (это официальный термин), похожие на «+»: «*» и «?». Они используются для обозначения от нуля и более количества вхождений и нуля или одного вхождения предыдущего символа, соответственно. Поэтому:

/eg*/

может найти «easy», «egocentric» и «egg», в то время, как

/Wil?/

может найти «Winne», «Wimpy», «Wilson» и «William», но не «Wendy» или «Wolf». Если все это будет немного неточным, вы можете также указать интервал чисел совпадений. Например, регулярное выражение

/jim{2,6}/

может найти «jimmy» и «jimmmmmy!», но не «jim». Числа в фигурных скобках указывают минимальное и максимальное значения интервала соответствия; вы можете не указывать верхний предел для бесконечного количества соответствий.

Двое для Танго

А сейчас вы узнаете, какие регулярные выражения есть, давайте посмотрим на их использование в скрипте. Объект String в JavaScript дает набор методов, которые поддерживают регулярные выражения. Первый из них — это метод search(), используемый для поиска строки для соответствия определенному регулярному выражению. Посмотрите на следующий пример:

<script language="JavaScript">
 
// определяем строку для поиска
 
var str = "The Matrix";
 
// определяем шаблон поиска
 
var pattern = /trinity/;
 
// ищем и возвращаем результат
 
if(str.search(pattern) == -1) 
 
{
 
   alert("Тринити не в Матрице");
 
} else 
 
{
 
   alert("Трините в Матрице на символе " + 
 
   str.search(pattern));
 
}
 
</script>

Если вы выполните этот скрипт, вы увидете следующее:

Тринити не в Матрице

Метод search() возвращает позицию подстроки, соответствующую регулярному выражению или -1 в случае отсутствия такого соответствия. В нашем примере видно, что шаблона «trinity» в строке «The Matrix» нет, поэтому мы и получаем сообщение об ошибке. А теперь посмотрим, что будет, если изменить регулярное выражения так, чтобы результат был положительным:

<script language="JavaScript">
 
// определяем строку для поиска
 
var str = "The Matrix";
 
// определяем шаблон поиска
 
var pattern = /tri/;
 
// ищем и возвращаем результат
 
if(str.search(pattern) == -1) 
 
{
 
   alert("Тринити не в Матрице");
 
} else 
 
{
 
   alert("Трините в Матрице на символе " + 
 
   str.search(pattern));
 
}
 
</script>

В этот раз, интерпретатор JavaScript найдет соответствие (и место, где его нашел). Вот результат:

Trinity located in The Matrix at character 7

Игра, Установка, Соответствие

Объект String также предоставляет метод match(), который может расцениваться, как близкий родственник метода search(). В чем же разница? Что ж, вы уже увидели, что метод search() возвращает позицию, где находится соответствие. Метод match() работает немного по-другому: он применяет шаблон к строке и возвращает массив найденных значений.

Смущены? Посмотрите на следующий пример:

<script language="JavaScript">
 
// определяем строку
 
var str = "Mississippi";
 
// определяем шаблон
 
var pattern = /is./;
 
// проверяем на вхождение
 
// помещаем результат в массив
 
var result = 
 
str.match(pattern);
 
// display matches
 
for(i = 0; i < result.length; i++) 
 
{
 
 alert("Соответствие #" + (i+1) + ": " + result[i]);
 
}
 
</script>

Просмотрите этот пример в браузере, и вы получите сообщение, показывающее первый результат соответствия. Вот такой:

Соответствие #1: iss

В этом примере, я залал регулярное выражение «is.». Он найдет строку «is», за которой следует любой символ (оператор «.» в конце шаблона находит все что угодно в строке). Если вы посмотрите на строку, в которой мы производили поиск, вы увидите, что в ней есть два вхождения этого шаблона. В то время, как код возвращает только 1.

Почему?

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

<script language="JavaScript">
 
// определяем строку
 
var str = "Mississippi";
 
// определяем шаблон
 
// и глобальный модификатор
 
var pattern = /is./g;
 
// проверяем на вхождение
 
// помещаем результат в массив
 
var result = 
 
str.match(pattern);
 
// display matches
 
for(i = 0; i < result.length; i++) 
 
{
 
 alert("Соответствие #" + (i+1) + ": " + result[i]);
 
}
 
</script>

И теперь, когда вы попробуете этот пример, вы должны увидеть два сообщения, показывающих нахождение двух вхождений этого шаблона в строку. Добавленный модификатор «g» обеспечивает нахождение всех вхождений шаблона в строку и сохранение в возвращаемый массив. Далее в этой статье, я покажу вам несколько других модификаторов.

Найти и Уничтожить

Предыдущий набор примеров демонстрировал возможности поиска объекта String. Но это ещё не все! Вы также можете осуществлять операции поиска и замены с помощью метод replace(), который принимает регулярное выражение и значение для его замены. Вот так:

<script language="JavaScript">
 
// определяем строку
 
var str = "Welcome to the Matrix, Mr. Anderson";
 
// заменяем одну строку на другую
 
str 
 
= str.replace(/Anderson/,"Smith");
 
// показываем новую строку
 
alert(str);
 
</script>

Если вы загрузите этот пример в браузере, вы увидите, что подстрока «Anderson» была занесена строкой «Smith». Что и иллюстрирует результат:

Welcome to the Matrix, Mr. Smith

Помните, как мы использовали модификатор «g» для поиска нескольких вхождений шаблона в строку? Переходим на следующую ступень — вы можете использовать его для замены нескольких вхождений шаблона в строку:

<script language="JavaScript">
 
// определяем строку
 
var str = "yo ho ho and a bottle of gum";
 
// возвращает "yoo hoo hoo and a bottle of gum"
 
alert(str.replace(/os/g, "oo "));
 
</script>

Здесь мета-символ «s» обозначает пробелы после «yo» и «ho» и заменяет на «oo».

Также, вы можете использовать нечувствительный к регистру поиск по шаблону — просто добавьте модификатор «i» в конце шаблона. Следующий пример демонстрирует, как это делается:

<script language="JavaScript">
 
// определяем строку
 
var str = "he He hE HE";
 
// возвращает ho ho ho ho
 
alert(str.replace(/he/gi, "ho"));
 
</script>

Разделяя

Объект String также предоставляет метод split(), который может быть использован для разделения одной строки на отдельные части на основе особого значения разделения. Эти части затем помещаются в массив для дальнейшей обработки. Демонстрирует это следующий пример:

<script language="JavaScript">
 
// определяем строку
 
var friends = "Joey, Rachel, Monica, Chandler, Ross, 
 
Phoebe";
 
// разделяем на части с помощью запятых
 
var arr = friends.split(", ");
 
// проходим по массиву и выводим
 
// каждое значение
 
for (x=0; x < arr.length; x++)
 
{
 
   alert("Hiya, " + arr[x]);
 
}
 
</script>

В JavaScript версии 1.1 и ниже, вы можете использовать только строковые значения в качестве разделителей. JavaScript 1.2 меняет все это, теперь вы можете разделять строки даже на основе регулярных выражений.

Чтобы лучше это понять, рассмотрим следующую строку, которая демонстрирует распространенную проблему: неравное количество пробелов между значениями разделения:

Neo
 
| Trinity   |Morpheus    |  
 
Smith|  Tank

Здесь символ «|» используется для разделения различных имен. И количество пробелов между разными «|» разное — это означает, что прежде, чем вы сможете использовать разные элементы строки, вы вынуждены удалить лишние пробелы вокруг них. Разделение с использованием регулярного выражения в качестве разделителя является элегантным решением этой проблемы, что мы и видим на следующем примере:

<script language="JavaScript">
 
// определяем строку
 
var str = "Neo| Trinity   
 
|Morpheus    |  Smith|  Tank";
 
// определяем шаблон
 
var pattern = /s*|s*/;
 
// разделяем строку с помощью
 
// регулярного выражения в
 
// качестве разделителя
 
result = 
 
str.split(pattern);
 
// проходим получившийся массив
 
for(i = 0; i < result.length; i++) 
 
{
 
 alert("Символ #" + (i+1) + ": " + result[i]);
 
}
 
</script>

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

Объекты в зеркале заднего вида

Итак, все примеры в этой статье связаны с объектом String для демонстрации мощи реализации регулярных выражений в JavaScript. Но JavaScript также предоставляет базовый объект, RegExp, смысл существования которого — поиск по шаблону в строках и переменных.

Этот объект имеет три полезных метода. Вот они:

test() — проверяет строку на вхождение по шаблону.

exec() — возвращает массив найденных вхождений в строке, позволяя расширенную работу с регулярными выражениями

compile() — после того, как регулярное выражение связано с объектом RegExp.

Давайте рассмотрим простой пример:

<script language="JavaScript">
 
// определяем строку
 
var str = "The Matrix";
 
// создаем объект RegExp
 
var character = new RegExp("tri");
 
// ищем по шаблону в строке
 
if(character.test(str)) {
 
    alert("User 
 
          located in The Matrix.");
 
} else {
 
    alert("Sorry, user is not in The 
 
           Matrix.");
 
}
 
</script>

Это похоже на один из самых первых примеров этой статье. Тем не менее, как вы видите, он имеет совершенно другую реализацию.

Основное отличие находится в том, что создается объект RegExp для поиска с помощью регулярного выражения. Он создается с помощью ключевого слова «new», следующего за конструктором объекта. По определению, конструктор принимает два параметра: шаблон для поиска и модификаторы, если они имеют место быть (в этом примере их нет).

Следующим шагом после создания объекта, является его использование. Здесь мы использовали метод test() для поиска вхождения по шаблону. По умолчанию, этот метод принимает строковую переменную и сравнивает её с шаблоном, переданным в конструктор объекта. В случае нахождения соответствия, он возвращает true, в противном же случае false. Очевидно, что это более логичная реализация, чем использование метода search() объекта String.

Раз Миссиссипи, два Миссиссипи…

Следующий метод, который мы рассмотрим — это exec(). Поведение этого метода похоже на то, что делает метод match() объекта String. Посмотрим:

<script language="JavaScript">
 
// определяем строку
 
var place = "Mississippi";
 
// указываем шаблон
 
var obj = /is./;
 
// ищем вхождение,
 
// помещаем результат в массив
 
result = 
 
obj.exec(place);
 
// показываем результат
 
if(result != null) {
 
 alert("Found " + result[0] 
 
+ " at " + result.index);
 
}
 
</script>

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

Главное различие между методами match() и exec() в передаваемых параметрах. Первый требует шаблон в качестве аргумента, второй же требует строку для проверки.

И это ещё не все. У метода exec() есть возможность продолжить поиск по строке для нахождения аналогичного вхождения без указания модификатора «g». Протестируем эту возможность с помощью следующего примера:

<script language="JavaScript">
 
// определяем строку
 
var place = "Mississippi";
 
// определяем шаблон
 
var obj = /is./;
 
// ищем все вхождения в строку
 
// показываем результат
 
while((result = 
 
   obj.exec(place)) != null) {
 
    alert("Found " + result[0] + " at " + 
 
          result.index);
 
}
 
</script>

Итак, что у нас есть здесь? Для начинающих: я использовал цикл «while» для вызова метода exec() до тех пор, пока не достигнут конец строки (на котором объект вернет null и цикл закончится). Это возможно, потому что каждый раз, вызывая exec(), объект RegExp продолжит поиск с того места, на котором закончил.

Но это все теория — такой код не будет работать ни в Internet Explorer, ни в NetScape Navigator, так что используйте его осторожно. Так что этот код чисто теоретический, по крайней мере до тех пор, пока не исправили ошибку (наверняка уже исправили — примеч. переводчика).

Другая интересная особенность этого кода заключается в создании объекта RegExp. Вы наверняка заметили, что, в отличие от предыдущего примера, здесь не используется конструктор для создания объекта. Вместо этого, шаблон просто применяется к переменной. Думайте об этом просто как о более коротком способе создания объекта RegExp.

Замены в ходе работы

Вы могли заметить в предыдущих примерах использования объекта RegExp, что регулярное выражение указывается во время создания объекта. Поэтому вы можете заинтересоваться, а что если требуется поменять шаблон позже?

Что ж, это не проблема. Метод compile() позволяет пользователю изменить шаблон объекта RegExp в выполнении поиска. Посмотрим:

<script language="JavaScript">
 
// определяем строку
 
var str = "The Matrix";
 
// определяем шаблон
 
var pattern = "trinity";
 
// define object
 
var character = new RegExp(pattern);
 
// смотрим вхождения
 
if(character.test(str)) {
 
alert("Looking for " + 
 
    pattern + "...User located in The Matrix");
 
} else {
 
alert("Looking for " 
 
    + pattern + "...Sorry, user is not in The Matrix");
 
}
 
// меняем шаблон
 
var pattern = "tri";
 
character.compile(pattern);
 
// смотрим на вхождение и показываем результат
 
if(character.test(str)) 
 
{
 
   alert("Looking for " 
 
     + pattern + "...User located in The Matrix");
 
} 
 
else {
 
   alert("Looking for " + pattern 
 
     + "...Sorry, user is not in The Matrix");
 
}
 
</script>

Учтите, что использование метода compile() для динамического изменения шаблона, связано с объектом RegExp.

Работаем с формами

Теперь, когда вы знаете, как это все работает, давайте рассмотрим более практичный пример того, как вы можете применить полученные знания с пользой. Следующий пример, который показывает HTML-форму, запрашивающую у пользователя информацию о кредитной карте и электронной почты для выполнения покупки:

<html>
 
<head>
 
<script 
 
language="Javascript">
 
// требует регулярное выражение
 
// в качестве параметра
 
function checkField(theForm, 
 
theField, theFieldDisplay, objRegex) {
 
 objField = eval("document." + theForm + 
 
		"." + theField);
 
 if(!objRegex.test(objField.value))  {
 
     alert ("Please 
 
           enter a valid " + 
 
	   theFieldDisplay + "");
 
 objField.select();
 
 objField.focus();
 
 return (false);
 
 }
 
return (true);
 
}
 
// регулярные выражения для различных
 
// полей формы
 
// номер кредитной карты
 
// должен содержать 20 цифр
 
objPatCCNum = 
 
/^[0-9]{20}$/;       
 
// дата окончания действия
 
// кредитной карты
 
// должна состоять из месяца 
 
// от 01 до 12 и года от 2003 до 2010
 
objPatCCDOE = 
 
/^([0][1-9]|[1][1-2])/20(0[3-9]|10)$/; 
 
// пин-код кредитной карты 
 
// должен быть численным
 
objPatCCPin = 
 
/^[0-9]+$/;
 
// адрес e-mail
 
// должен быть в формате user@host
 
objPatCCEmail 
 
=
 
/^([a-zA-Z0-9])+([.a-zA-Z0-9_-])*@([a-zA-Z0-9_-])+(.[a-zA-Z0-9_-]+)+/;
 
// проверяем различные поля формы
 
function checkForm(theForm)
 
{
 
if(checkField(theForm, "cc_num", "Credit card number", objPatCCNum) 
 
&&
 
checkField(theForm, "cc_doe", "Date of expiry", objPatCCDOE) 
 
&&
 
checkField(theForm, "cc_pin", "PIN code", objPatCCPin) 
 
&&
 
checkField(theForm, "cc_email", "Email address", objPatCCEmail)) 
 
{
 
 	return true;
 
} else {
 
	return false;
 
}
 
}
 
</script>
 
</head>
 
<body>
 
<h2>Информация о кредитной карте</h2>
 
<form 
 
name="frmCCValidation" 
 
onSubmit="return checkForm('frmCCValidation');">
 
Номер кредитной карты:<br>
 
<input name="cc_num" type="text">
 
<p>
 
Тип кредитной карты <br>
 
<select name="cc_type">
 
<option 
 
value="Visa">Visa</option>
 
<option 
 
value="Mastercard">Mastercard</option>
 
<option 
 
value="AmericanExpress">American 
 
Express</option>
 
</select>
 
<p>
 
Дата окончания действия (мм/гггг) <br>
 
<input name="cc_doe" 
 
type="text">
 
<p>
 
Пин-код <br>
 
<input name="cc_pin" type="text">
 
<p>
 
Адрес электронной почты<br>
 
<input name="cc_email" type="text">
 
<p>
 
<input type="submit" value="Отправить">
 
</form>
 
</body>
 
</html>

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

В заключение

После прочтения, я почти уверен, что вы взглянули на JavaScript совсем с другой стороны. Язык, который вы увидели, не был наиболее часто используемым в замене изображений и определении браузера. Теперь это мощный инструмент, помогающий выполнять поиск по шаблону на стороне клиента быстро и эффективно.

Я начинал с простого введения в регулярные выражения и быстро перешел к методам search() и replace() объекта String. Эти функции принимают регулярное выражение в качестве параметра и позволяют производить операции поиска и замены со строковыми переменными. Это привело нас к рассказу об объекте RegExp. Этот объект дает набор методов и свойств, позволяющих программистом повысить уровень мощности использования регулярных выражений в JavaScript.

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

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

«Stringing Things Along»

«Pattern Matching and Regular Expressions»

«Regular Expressions for client-side JavaScript, a free online quick reference»

Веб-сервер lighttpd

До недавнего времени, Apache не имел серьезных конкурентов с открытым исходным кодом. В последнем обзоре веб-серверов, проведенном Netcraft, мы можем видеть нечто новое. Как и обычно, Apache возглавляет список, Microsoft IIS второй, а знаменитый unknown — третий. Четвертым является Sun Java Web Server (ранее известный как ONE, бывший iPlanet, бывший Netscape). Но пятым номером, обслуживая около 1,4 миллиона сайтов, нечто под названием lighttpd. Откуда это взялось? Мы изучим историю lighttpd, начальную установку и настройку, а также некоторые взгляды на будущее проекта.

Вы можете произносить это название как лайт-ти-пи-ди или, меньше плюясь, лайти. Как бы вы не говорили/выплевывали название этого веб-сервера, вы найдете много информации на его веб-сайте, вики, блоге, или на форумах. lighttpd проектировался как высокоэффективный веб-сервер с низким использованием ресурсов. Он использует гораздо меньше памяти, чем Apache и, как правило, работает быстрее. lighttpd тихо усиливает много высоконагруженных сайтов, включая YouTube, Wikipedia, Meebo и A List Apart; вы увидете, что он часто используется вместо Apache вместе с популярными инструментами, такими как Ruby on Rails и Trac.

Что не так с Apache?

Не смотря на свою популярность, иногда использование Apache не является лучшим решением. Apache предоставляет различные Мульти-Процессные Модели (MPMs) для использования в различных средах работы. Модель prefork — наиболее популярная в Linux — создает определенное число процессов Apache при его запуске и управляет ими в пуле. Альтернативной моделью является worker, которая использует несколько потоков вместо процессов. Хотя потоки легче, чем процессы, вы не можете их использовать до тех пор, пока весь ваш сервер не будет безопасен для потоков (thread safe). Хотя Apache и mod_php — безопасны для потоков, это не гарантирует, что все сторонние модули могут использоваться. Сайт PHP не одобряет использование Apache 2 с потоковой MPM; это замедляет переход разработчиков с Apache 1.3 на 2.x. А модель prefork имеет свои проблемы: каждый процесс (Apache + PHP + сторонние модули) занимает много памяти (30 МБ не является чем-то необычным). Когда вы увеличиваете число одновременных процессов Apache, доступная вам память может быстро кончиться.

lighttpd в тумане.

Некоторые сайты параллельно обрабатывают тысячи файлов, будучи при этом ограничены в памяти и максимальным числом потоков или процессов. Дэн Кегел (Dan Kegel) подробно описал проблемы при обработки тысяч одновременных запросов на своей странице в C10K problem. В 2003 немецкий разработчик MySQL по имени Ян Кнешке (Jan Kneschke) заинтересовался в этой проблеме и решил, что сможет написать веб-сервер, который будет быстрее Apache, сфокусировавшись на правильных методиках. Он спроектировал lighttpd как один процесс с одним потоком и неблокирующимся вводом-выводом. Вместо select, он использовал быстрейшие обработчики событий в целевой системе: poll, epoll, kqueue или /dev/poll. Он выбрал безэкземлярные системные вызовы, как sendfile вместо read и write. В течение нескольких месяцев lighttpd стал обрабатывать статичные файлы быстрее, чем Apache.

Следующим шагом было обработать динамические (CGI) приложения, в частности PHP. Кнешке сдул пыль с FastCGI, разработанном Open Market в самых ранних днях существования интернета, в целях улучшения работы CGI. Вместо того, чтобы веб-сервер запускал ту же внешнюю CGI-программу при каждом вызове, FastCGI по существу был демоном для предварительного запуска CGI-приложения и управления связью между ним и веб-сервером. Это было быстрее, но Perl и PHP позже были внедрены в Apache в качестве модулей, которые были даже быстрее и получали доступ к внутренним действиям Apache по обработке HTTP. FastCGI для Apache стал пренебрегаться, но после того, как он был добавлен в lighttpd и к нему подключили PHP, его производительность равнялась или превышала ту, что показывал Apache с mod_php. В качестве дополнения, в реализацию lighttpd была добавлена автоматическая балансировка загрузки.

lighttpd был также расширен модулями для управления виртуальными хостами, переадресации, перезаписи URL, аутентификации и других полезных вещей. В большинстве случаев, все, что вы можете сделать в Apache, вы можете сделать в lighttpd. В следующих разделах я покажу, как установить и настроить lighttpd с оглядкой на Apache.

Установка lighttpd.

Давайте установим lighttpd и поковыряемся в нем образными палочками. Страница установки на вики дает примеры установки бинарников или из исходников для различных дистрибутивов Linux. Для разработчиков с волосатой грудью (эти волосы не обязательно должны быть вашими), полная установка из исходников выглядит так:

    # wget http://www.lighttpd.net/download/lighttpd-1.4.13.tar.gz
    # tar xvzf lighttpd-1.4.13.tar.gz
    # cd lighttpd-1.4.13
        # ./configure
    # make
    # make install

Это установит lighttpd в /usr/local. Если сборка не удалась, проверьте, что требуемые пакеты разработки pcre и zlib установлены в вашей системе.

Если вы хотите запускать и останавливать lighttpd вручную, то вперед. А чтобы установить lighttpd как сервис вроде Apache, отредактируйте и установите скрипт инициализации:

    # sed -e 's/FOO/lighttpd/g' doc/rc.lighttpd &gt; lighttpd.init
    # chmod a+rx lighttpd.init
    # cp lighttpd.init /etc/init.d/lighttpd
    # cp -p doc/sysconfig.lighttpd /etc/sysconfig/lighttpd
    # install -Dp ./doc/lighttpd.conf /etc/lighttpd/lighttpd.conf
        # chkconfig lighttpd on

Начальная настройка.

С виду синтаксис файлов настройки lighttpd может иметь отличия от Apache. Примеры со страницы настройки из вики выглядят больше похожими на Perl (или PHP, или Python), нежели чем на стиль XML httpd.conf в Apache. Для простого сайта со статическими файлами, вам нужно указать те же вещи, что и в Apache: document root, расположение access и error логов и имена user и group для работы сервера. Вот эквивалентные варианты Apache (httpd.conf) и lighttpd (lighttpd.conf):

Apache:

DocumentRoot /var/www/html
 
CustomLog /var/www/logs/access
 
ErrorLog /var/www/logs/error
 
User www
 
Group www

lighttpd:

server.document-root = "/var/www/html"
 
accesslog.filename = "/var/www/logs/access"
 
server.errorlog = "/var/www/logs/error"
 
server.username = "www"
 
server.groupname = "www"
 
server.modules = ( "mod_accesslog" )

Механизм включения модулей в lighttpd похож на тот, что в Apache, так что файл lighttpd.conf не должен расти. Чтобы использовать дополнительный модуль, вам нужно включить его и установить его опции. В Apache это делается с помощью LoadModule, а lighttpd просто включает незакомментированный модуль в массиве server.modules. Пока что вам нужен только mod_accesslog.

Аутентификация и авторизация.

lighttpd не поддерживает файлы .htaccess, так что вам нужно указывать все настройки в файле lighttpd.conf, или в файлах, к нему подключенным. Он понимает файлы пользователей Apache для простой и общей аутентификации, но поддержка групповых файлов ещё не реализована. Вот пример, как защитить паролем высокоуровневый каталог под названием special:

Apache:

  AuthName "My Special Directory"
 
  AuthType Basic
 
  AuthUserFile /var/www/passwords/users
 
  Order deny,allow
 
  require valid-user

lighttpd:

auth.backend = "htpasswd"
 
auth.backend.htpasswd.userfile = "/var/www/passwords/users"
 
auth.require = ( "/special/" =&gt;
 
  (
 
  "method"   =&gt; "basic",
 
  "realm"    =&gt; "My Special Directory",
 
  "require"  =&gt; "valid-user"
 
  )
 
)

Виртуальные хосты.

Вот другая задача для вашего нагруженного и недооцененного веб-сервера: управлять двумя сайтами с названиями scratch.example.com и sniff.example.com:

Apache:

NameVirtualHost *
 
  ServerName "scratch.example.com"
 
  DocumentRoot "/var/www/hosts/scratch/docs"
 
  ServerName "sniff.example.com"
 
  DocumentRoot "/var/www/hosts/sniff/docs"

lighttpd:

$HTTP["host"] == "scratch.example.com" {
 
  server.document-root = "/var/www/hosts/scratch/docs/" }
 
$HTTP["host"] == "sniff.example.com" {
 
  server.document-root = "/var/www/hosts/sniff/docs/" }

Это сложный путь решения этой задачи. Если вы управляете многими хостами, более коротким вариантом будет использование модуля виртуальных хостов:

Apache:

LoadModule vhost_alias_module modules/mod_vhost_alias.so
 
VirtualDocumentRoot /var/www/hosts/%1/docs

lighttpd:

server.modules = ( ..., "mod_evhost", ... )
 
evhost.path-pattern = "/var/www/hosts/%3/docs"

Server-Side Includes (SSI)

Первым шагом к динамическому контенту будет простое включение SSI для файлов с расширением .shtml:

Apache:

AddHandler server-parsed .shtml

lighttpd:

server.modules = ( ..., "mod_ssi", ... )
 
ssi.extension = ( "shtml" )

PHP

lighttpd оптимизирует производительность обработки статичного файла с помощью выгрузки динамического контента, загружающего ЦПУ в другой процесс. Вместо обработки PHP внутри, как делает Apache с mod_php, lighttpd передает эту задачу FastCGI. Следующие примеры конфигурации превращают скучные, безжизненные .php-файлы в живые PHP скрипты. Чтобы ознакомиться с более пикантными деталями, чем те, что мы можем показать на этом сайте, посмотрите эту страницу.

Apache:

LoadModule php5_module modules/libphp5.so
 
AddType application/x-httpd-php .php

lighttpd:

server.modules = ( ..., "mod_fastcgi", ... )
 
fastcgi.server =
 
  ( ".php" =&gt;
 
    ( "localhost" =&gt;
 
      (
 
      "socket" =&gt; "/tmp/php-fastcgi.socket",
 
      "bin-path" =&gt; "/usr/local/bin/php"
 
      )
 
    )
 
  )

Достоинства lighttpd

lighttpd включает в себя модули, эквивалентные модулям Apache для сжатия, просмотра каталогов, пользовательских каталогов, SSL, WebDAV, перезаписи URL и переадресации. Вы можете прочитать об этом на сайте. Другие интересные модули lighttpd являются уникальными.

Если вы хотите сделать мини-YouTube, мы можете параллельно показывать тысячи флеш-роликов с помощью модуля mod_flv_streaming. YouTube обрабатывает его собственные статические файлы с помощью lighttpd, хотя ещё не использует этот модуль.

Если у вас есть такой сайт с большим количеством флеш-файлов, как вам защититься от хотлинкинга? Решением этой проблемы в lighttpd, подходящим для любых типов файлов, является mod_secdownload. Вы пишете функцию (примеры по ссылке написаны на PHP и Ruby) для генерирования специального URL, а модуль обрабатывает этот URL для предоставления доступа к определенному файлу на определенный период времени.

Лайти Один Пять Ноль.

В данный момент Кнешке разрабатывает версию 1.5.0, которая увеличит производительность и гибкость. Новая подсистема ввода-вывода улучшает производительность с помощью тредов (в данном случае треды имеют смысл) и асинхронный ввод-вывод, будь то POSIX, собственный в Linux или пользовательская оболочка gthread в glib 2.0.

Модуль mod_proxy_core объединяет три бэкенд модуля: mod-proxy, mod-fastcgi и mod-scgi. Отделение протоколов от основной обработки дает балансировку нагрузки (четырех видов!), преодоление отказа, удержание соединения и внутреннее формирование очереди к основной функции прокси.

Ожидается, что новый модуль под названием mod_magnet сыграет большую роль в будущем lighttpd. Он дает доступ к разным фазам HTTP запроса и ответа, включая задачи вроде перезаписи URL и генерации контента. Интересным выбором было использование в нем встроенного скриптового языка Lua вместо сложной грамматики, как в mod_rewrite Apache. Посмотрим, понравится ли это разработчикам, или они будут придерживаться знакомым, хотя иногда и сложным, правилам Apache.

Куда Лайти?

Кнешке видит будущее lighttpd в двух направлениях:

— Высокопроизводительные, широкодоступные поставщики контента

— Встроенные сервера, кросс-компиляция, использовать мало памяти

После 1.5.0, mod_magnet будет предоставлять больше динамической конфигурации сервера. Это может привлечь некоторых пользователей Apache, отказавшихся от перехода на lighttpd из-за отсутствия поддержки .htaccess. Я с нетерпением жду запланированную поддержку Comet — обратная сторона AJAX, в которой сервер обновляет клиента при поступлении новых данных. Это имеет приложения к панелям управления веб-приложений, чатам и другим высокоинтерактивным приложениям. С помощью AJAX и Comet, веб-приложения могут выглядеть гораздо более похожими на приложения с традиционным GUI. Но даже без этих новых возможностей, lighttpd уже является сильным конкурентом для Apache, особенно при ограничениях в памяти или обработке множества статических файлов. Легкость — это хорошо.

Отключить autorun для разных флешек и других дисков

Кстати, если кто еще не отключил автозапуск для дисков в своей windows, тот очень рискует быть зараженным нехорошим вирусом. Дело в том, что они используют файл autorun.inf копируя его в корень вашей usb-flash (как вариант) и затем, когда вы ее вставите в свой компьютер файл запустится автоматически и заразит вас. Как отключить сие дейсвтие мы расскажем вам в нескольких словах.

Вот ниже приводится текст

REGEDIT4
 
[HKEY_LOCAL_MACHINESYSTEMCurrentControlSetServicesCdrom]
“AutoRun”=dword:00000000

Скорпируйте его в Блокнот. Затем сохраните обязательно с расширением .reg. Затем кликните два раза по этому файлу и согласитесь с внесением данных в реестр windows. И помните — не отключив autorun вы подвергаете себя, свой компьютер и своих близких большой опасности.

Настройка синхронизации времени на сервере FreeBSD

Прописываем в /etc/rc.conf

    ntpdate_enable="YES"
    ntpd_enable="YES"

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

Далее в /etc/ntp.conf пишем (рекомендуемые сервера с ntp.org)

    server 0.pool.ntp.org
    server 1.pool.ntp.org
    server 2.pool.ntp.org
    restrict 127.0.0.1

Строка restrict 127.0.0.1 разрешает управление демоном ntpd только с localhost (например для работы утилиты ntpd), можно вообще закрыть доступ написав restrict default ignore или разрешить только из локальной сети restrict 192.168.1.0 mask 255.255.255.0 nomodify notrap — разрешит синхронизацию времени, но запретит настраивать сервер или быть равноправными участниками синхронизации времени.

И собственное все, подводим часы

    # /etc/rc.d/ntpdate start
    Setting date via ntp.
    23 Jan 00:56:48 ntpdate[928]: step time server 193.125.143.140 offset 0.027002 sec

и запускаем демон

    # /etc/rc.d/ntpd start
    Starting ntpd.

Смотрим что ntpd запустился нормально

    # ps ax|grep ntpd
    1044 ?? Ss 0:00,04 /usr/sbin/ntpd -c /etc/ntp.conf -p /var/run/ntpd.pid -f /var/db/ntpd.drift
    # tail /var/log/messages
    Jan 23 00:57:09 lola ntpd[1043]: ntpd 4.2.4p5-a Mon Jan 5 16:59:33 IRKT 2009 (1)
    # ntpq -p
         remote           refid      st t when poll reach   delay   offset  jitter
    ==============================================================================
     petrel.telecom. 192.36.143.150   2 u   60   64    1  119.017  5082.52   0.004
     hornet.itconnec 89.111.168.177   4 u   59   64    1  134.423  5086.05   0.004
     jane.telecom.mi 192.36.143.151   2 u   58   64    1  120.222  5086.08   0.004

Источник