🧩 Принципы и паттерны безопасной разработки: OCP и fail-closed
(Open/Closed Principle) — классы и функции открыты для расширения, но закрыты для модификации. Говоря проще: проектируем приложение так, чтобы для новых фичей требовались минимальные изменения в уже существующем (протестированном и стабильном, ну... как правило) коде.
С точки зрения безопасности, это снижает риски сломать уже существующие защитные меры и получить на ровном месте регрессии вроде:
• CWE-840 — логическая уязвимость из-за изменения условий
• CWE-489 — ослабление проверки при модификации кода
... и охапки прочих, на правах их прямых последствий.
🐛 Жизненное
В Apache HTTPD (CVE-2021-41773) разработчики изменили ядро обработки путей — и открыли обход директорий ../. Если бы новую логику добавили отдельным модулем, а не правили существующую, старая проверка осталась бы нетронутой.
Та же история с Dirty Pipe (CVE-2022-0847) в Linux: неаккуратная «оптимизация» существующего кода pipe нарушила старые гарантии → повышение локальных привилегий.
💡 Пример: PEP-750, t-строки и шаблоны
По мотивам предыдущего поста: t"..." создаёт объект Template, а его части (Interpolation) форматируются по format_spec. Здесь напрашивается типичная ошибка — дописывать в обработчике if/elif для новых форматов (HTML, SQL, shell). Каждый раз приходится лезть в уже написанный код, и, тем самым, нарушать OCP.
Плохой пример:
def render(t):
for part in t:
if spec == "html":
out.append(html_escape(v))
elif spec == "sql":
out.append(sql_param(v))
else:
out.append(format(v))
Новый формат → новая ветка → новый риск сломать там что-то ранее работавшее (вспоминаем goto fail;).
Хороший пример:
_handlers = []
def register(h): _handlers.append(h); return h
def render(t):
for it in t:
if isinstance(it, str): yield it
elif h := next((h for h in _handlers if h.supports(it)), None):
yield h.apply(it)
else:
raise ValueError(f"Unsupported spec: {it.format_spec}")
@register
class HtmlHandler:
def supports(self, it): return it.format_spec.startswith("html")
def apply(self, it): return html_escape(str(it.value))
Ядро неизменно — добавляем только новые обработчики, неизвестные спецификации блокируются (fail-closed*): безопаснее, предсказуемее, тестируется в разы проще.
⚠️ OCP — не догма
«Модификация» в OCP не про рефакторинг, баги или уязвимости. Если в существующем коде нашли нашли проблему, то нужно править. Безопасность и здравый смысл приоритетнее. OCP всё же — не тотальный запрет на изменения, а гигиена расширяемости: добавление фичей, без изменения того, что уже защищено и протестировано.
TL;DR:
• Стоит разумно следовать OCP, чтобы не сломать защиту, добавляя фичи.
• Расширять, а не модифицировать, если речь не идёт о рефакторинге, багах или уязвимостях.
• Из-за нарушений OCP «увидели свет» многие, в том числе именитые, CVE.
(Open/Closed Principle) — классы и функции открыты для расширения, но закрыты для модификации. Говоря проще: проектируем приложение так, чтобы для новых фичей требовались минимальные изменения в уже существующем (протестированном и стабильном, ну... как правило) коде.
С точки зрения безопасности, это снижает риски сломать уже существующие защитные меры и получить на ровном месте регрессии вроде:
• CWE-840 — логическая уязвимость из-за изменения условий
• CWE-489 — ослабление проверки при модификации кода
... и охапки прочих, на правах их прямых последствий.
🐛 Жизненное
В Apache HTTPD (CVE-2021-41773) разработчики изменили ядро обработки путей — и открыли обход директорий ../. Если бы новую логику добавили отдельным модулем, а не правили существующую, старая проверка осталась бы нетронутой.
Та же история с Dirty Pipe (CVE-2022-0847) в Linux: неаккуратная «оптимизация» существующего кода pipe нарушила старые гарантии → повышение локальных привилегий.
💡 Пример: PEP-750, t-строки и шаблоны
По мотивам предыдущего поста: t"..." создаёт объект Template, а его части (Interpolation) форматируются по format_spec. Здесь напрашивается типичная ошибка — дописывать в обработчике if/elif для новых форматов (HTML, SQL, shell). Каждый раз приходится лезть в уже написанный код, и, тем самым, нарушать OCP.
Плохой пример:
def render(t):
for part in t:
if spec == "html":
out.append(html_escape(v))
elif spec == "sql":
out.append(sql_param(v))
else:
out.append(format(v))
Новый формат → новая ветка → новый риск сломать там что-то ранее работавшее (вспоминаем goto fail;).
Хороший пример:
_handlers = []
def register(h): _handlers.append(h); return h
def render(t):
for it in t:
if isinstance(it, str): yield it
elif h := next((h for h in _handlers if h.supports(it)), None):
yield h.apply(it)
else:
raise ValueError(f"Unsupported spec: {it.format_spec}")
@register
class HtmlHandler:
def supports(self, it): return it.format_spec.startswith("html")
def apply(self, it): return html_escape(str(it.value))
Ядро неизменно — добавляем только новые обработчики, неизвестные спецификации блокируются (fail-closed*): безопаснее, предсказуемее, тестируется в разы проще.
*️⃣ Fail-closed (безопасный отказ) — принцип проектирования, при котором система в случае ошибки или неопределённости выбирает безопасное поведение, даже если это мешает работе.
Примеры:
• парсер не знает формат входных данных → отклоняет запрос;
• фильтр не смог проверить токен → доступ запрещён;
• обработчик t-строк встретил неизвестный `format_spec` → бросает исключение вместо неэкранированного вывода.
Такой подход предотвращает «тихие» обходы проверок и делает поведение системы предсказуемым даже при сбоях.
⚠️ OCP — не догма
«Модификация» в OCP не про рефакторинг, баги или уязвимости. Если в существующем коде нашли нашли проблему, то нужно править. Безопасность и здравый смысл приоритетнее. OCP всё же — не тотальный запрет на изменения, а гигиена расширяемости: добавление фичей, без изменения того, что уже защищено и протестировано.
TL;DR:
• Стоит разумно следовать OCP, чтобы не сломать защиту, добавляя фичи.
• Расширять, а не модифицировать, если речь не идёт о рефакторинге, багах или уязвимостях.
• Из-за нарушений OCP «увидели свет» многие, в том числе именитые, CVE.