<?xml version="1.0" encoding="utf-8"?> 
<rss version="2.0"
  xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
  xmlns:atom="http://www.w3.org/2005/Atom">

<channel>

<title>Заметки Андрея Гейна: posts tagged csv</title>
<link>https://andgein.ru/blog/tags/csv/</link>
<description>Блог Андрея Гейна: заметки о жизни, программировании, преподавании и дизайне</description>
<author></author>
<language>en</language>
<generator>Aegea 11.3 (v4134)</generator>

<itunes:subtitle>Блог Андрея Гейна: заметки о жизни, программировании, преподавании и дизайне</itunes:subtitle>
<itunes:image href="" />
<itunes:explicit></itunes:explicit>

<item>
<title>Заметка вторая. О сервисе на PHDays CTF</title>
<guid isPermaLink="false">2</guid>
<link>https://andgein.ru/blog/all/2-phdays-ctf-2017-service/</link>
<pubDate>Wed, 17 May 2017 15:23:38 +0100</pubDate>
<author></author>
<comments>https://andgein.ru/blog/all/2-phdays-ctf-2017-service/</comments>
<description>
&lt;p&gt;&lt;i&gt;Продолжение истории о &lt;a href="/blog/all/1-unicode-and-csv-in-python/"&gt;необычном поведении&lt;/a&gt; юникода и CSV в питоне&lt;/i&gt;&lt;/p&gt;
&lt;h2&gt;Для тех, кому лень читать предыдущую заметку или вспоминать, о чём там было&lt;/h2&gt;
&lt;p&gt;Если использовать стандартные питоновские &lt;a href="https://docs.python.org/3/library/codecs.html#codecs.open"&gt;codecs.open&lt;/a&gt;, &lt;a href="https://docs.python.org/3/library/csv.html#csv.DictWriter"&gt;csv.DictWriter&lt;/a&gt; и &lt;a href="https://docs.python.org/3/library/csv.html#csv.DictReader"&gt;csv.DictReader&lt;/a&gt;, то можно столкнуться с интересным поведением. Создаём в программе файл в кодировке UTF-8, пишем туда с помощью DictWriter, а затем читаем через DictReader. Если в данных встречались юникодные символы Pararaph separator или Line separator, то мы считаем больше записей, чем было записано, а их CSV-структура будет поломана.&lt;/p&gt;
&lt;p&gt;Читайте &lt;a href="/blog/all/1-unicode-and-csv-in-python/"&gt;первую заметку&lt;/a&gt; для подробностей.&lt;/p&gt;
&lt;h2&gt;Как из этого получился сервис для классического CTF&lt;/h2&gt;
&lt;p&gt;В апреле мы с ребятами из Хакердома как раз готовили онлайновый CTF для форума &lt;a href="https://phdays.com"&gt;PHDays&lt;/a&gt;. Темой был выбран интернет вещей, а сервисами были умный чайник, термометр, дверной замок, телевизор и даже холодильник. Мне достался последний, потому что я слишком люблю еду.&lt;/p&gt;
&lt;p&gt;Чтобы органично встроить уязвимость в сервис, мне нужно было сделать так, чтобы кто-то писал в CSV-файл, а кто-то другой — читал. В итоге сервис состоял из двух частей — веб-приложения для удобного управления человеком и API для других умных кухонных гаджетов, которым нужна информация от холодильника.&lt;/p&gt;
&lt;p&gt;Веб-интерфейс позволял пользователям регистрироваться и создавать холодильные камеры или просто холодильники. У каждого холодильника был владелец, и увидеть чужие просто так нельзя. В холодильники можно добавлять еду. Для этого сначала было необходимо зарегистрировать новый продукт (например, картошку или молоко) и указать, в чём он измеряется — в граммах, пачках или литрах. Выглядело это так:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;div class="fotorama" data-width="1122" data-ratio="2.5384615384615"&gt;
&lt;img src="https://andgein.ru/blog/pictures/2017-05-16_17-12-56-(2).png" width="1122" height="442" alt="" /&gt;
&lt;img src="https://andgein.ru/blog/pictures/2017-05-16_17-14-21.png" width="1125" height="676" alt="" /&gt;
&lt;img src="https://andgein.ru/blog/pictures/2017-05-16_17-23-56.png" width="1124" height="597" alt="" /&gt;
&lt;img src="https://andgein.ru/blog/pictures/2017-05-16_20-56-37.png" width="1127" height="398" alt="" /&gt;
&lt;img src="https://andgein.ru/blog/pictures/2017-05-16_20-58-25-(2).png" width="1159" height="584" alt="" /&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Кроме холодильников можно было создавать рецепты. Про каждый ингредиент рецепта известно, сколько и какого продукты нужно положить, а также что с ним надо сделать и сколько после этого подождать.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://andgein.ru/blog/pictures/2017-05-16_21-07-14.png" width="1122" height="669" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Рецепты были очень важны для функционирования сервиса, ведь в том самом API было всего две команды — получить список твоих холодильников и получить список рецептов, которые можно приготовить из еды, лежащей в одном из твоих холодильников. Звучит сложно, но идея очень простая — если у вас есть умная мультиварка, то она хочет узнать, какие блюда можно сегодня приготовить из продуктов, лежащих у вас в холодильнике.&lt;/p&gt;
&lt;p&gt;Принадлежность выдаваемых рецептов владельцу холодильника в API не проверялась, но должно было работать само: в рецепте должен присутствовать хотя бы один продукт из холодильника, а в холодильник мы можем добавлять только принадлежащие нам продукты. Моё молоко и молоко Васи — два разных продукта, я могу добавить в холодильники и рецепты только первое, а про второе даже не смогу узнать.&lt;/p&gt;
&lt;h2&gt;Уязвимость&lt;/h2&gt;
&lt;p&gt;Итак, где же тут наши подозреваемые — юникод и CSV?&lt;/p&gt;
&lt;p&gt;Сервис API не имеет доступа к базе данных, поэтому информация для него складывалась веб-приложением в специальные CSV-файлы, откуда считывались API-приложением раз в секунду. Вот пример такого файла:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;id,recipe_id,food_type_id,what_to_do,count,pause_after
1,10,15,Yhws aqx vpjhtlyhw ruv tbcyis,11,7
2,11,16,Ok jbso gtpzs ndgoz udeksmvk,10,16
3,12,17,Y ap mculltvedfwwabbnnnco um,6,19
4,13,18,cniyvdjsyuctaamupp zm  qj nwvm,10,9
5,14,19,Kfdvm ref wtb pdtitb,14,15&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;В нём хранятся ингредиенты рецептов. Столбцы, соответственно — id ингредиента, id рецепта (сами рецепты лежат в другом файле), id продукта, что нужно сделать, сколько взять продукта и какую выдержать после этого паузу.&lt;/p&gt;
&lt;p&gt;Давайте посмотрим, что будет, если в поле what_to_do добавить тот самый Paragraph separator:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;6,15,20,blablabla 13503,14,20&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;При чтении этот файл будет выглядеть для DictReader’а как&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;6,15,20,blablabla 
13503,14,20&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;То есть так, будто мы добавили продукт №20 в рецепт №14. Продукт №20 принадлежит нам, значит, мы смогли добавить нашу еду в чужой рецепт №14! Теперь API сможет вывести этот рецепт, если в каком-нибудь из наших холодильников будет лежать продукт №20.&lt;/p&gt;
&lt;p&gt;Для полноты картины покажу, как выглядели сохранение и загрузка данных в CSV.&lt;/p&gt;
&lt;h3&gt;Сохранение:&lt;/h3&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;def dump_model_to_file(model, filename):
    # Use Django API to get all model fields
    columns = [field.attname for field in model._meta.fields]

    with codecs.open(filename, &amp;#039;w&amp;#039;, encoding=&amp;#039;utf-8&amp;#039;) as opened_file:
        # Open csv writer and dump header: special row with columns names
        writer = csv.DictWriter(opened_file, fieldnames=columns)
        writer.writeheader()

        # Iterate over all objects of the model
        for obj in model.objects.all():
            object_dict = {}
            for column in columns:
                # Don&amp;#039;t worry about newlines (\n and \r): csv.DictWriter will enclose such strings in quotes (&amp;quot;)
                # So I think there is no vulnerability here
                object_dict[column] = str(getattr(obj, column, &amp;#039;&amp;#039;))

            # Dump dictionary for current object
            writer.writerow(object_dict)&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;Загрузка:&lt;/h3&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;class Model:
    def __init__(self, dictionary):
        for key, value in dictionary.items():
            if value is None:
                value = &amp;#039;0&amp;#039;
            if value.isnumeric():
                value = int(value)
            setattr(self, key, value)


def load_models_from_file(filename):
    objects = {}
    with codecs.open(filename, &amp;#039;r&amp;#039;, encoding=&amp;#039;utf-8&amp;#039;) as opened_file:
        reader = csv.DictReader(opened_file)
        for row in reader:
            objects[model.id] = Model(row)
    return objects&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Ещё две уязвимости&lt;/h2&gt;
&lt;p&gt;В Холодильнике мной была заложена ещё одна уязвимость. Но так получилось, что в итоге уязвимостей оказалось не две, а три. Так бывает на CTF, и в этом нет ничего страшного. Иногда бывает обидно, что ты заложил сложную уязвимость, а случайно оставил простую. Но в данном случае всё случилось удачно: незапланированная уязвимость была проще первой, а запланированная — совсем элементарной (правда, позволяла украсть только 20% флагов). Так как в целом соревнование получилось очень сложным, то появление одной незапланированной уязвимости средней сложности сыграло нам на руку.&lt;/p&gt;
&lt;p&gt;Итак, сначала о запланированной уязвимости. Каждый сервис находился в своём докер-контейнере. Докер (docker) — это удобный способ изолировать своё приложение от других, в линуксе работает за счёт его фирменных LXC-контейнеров и ограничений в cgroup. Подробнее о докере можно почитать на &lt;a href="https://docker.com/"&gt;официальном сайте&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Мой сервис состоял из четырёх докер-контейнеров: для веб-приложения, для API-приложения, для базы данных и для веб-сервера nginx. Во время старта первого накатывались миграции, собиралась статика и выполнялись другие служебные команды. В том числе такая:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;# DON&amp;#039;T RUN IT IN PRODUCTION. SOME EVIL GUYS CAN BRUTEFORCE PASSWORD AND WHO KNOW WHAT HAPPENS...
echo &amp;quot;[+] [DEBUG] Django setup, executing: add superuser&amp;quot;
PGPASSWORD=${POSTGRES_PASSWORD} psql -U ${POSTGRES_USER} -h ${POSTGRES_HOST} -c &amp;quot;INSERT INTO auth_user (password, last_login, is_superuser, username, first_name, last_name, email, is_staff, is_active, date_joined) VALUES (&amp;#039;pbkdf2_sha256\$36000\$k36V24q60mNo\$v5og9qcgc2sqkVwGjZDKNK+wcJy60ix8DIt9E8Yg48c=&amp;#039;, &amp;#039;1970-01-01 00:00:00.000000&amp;#039;, true, &amp;#039;admin&amp;#039;, &amp;#039;admin&amp;#039;, &amp;#039;admin&amp;#039;, &amp;#039;admin@admin&amp;#039;, true, true, &amp;#039;1970-01-01 00:00:00.000000&amp;#039;) ON CONFLICT (username) DO NOTHING&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Эта команда добавляет напрямую в базу данных супер-пользователя с логином admin. Его пароль мы не знаем, так что сразу зайти под ним не можем, но доступен хеш от пароля: ‘pbkdf2_sha256$36000$k36V24q60mNo$v5og9qcgc2sqkVwGjZDKNK+wcJy60ix8DIt9E8Yg48c=’. Подбор пароля не занимает много времени, так как он словарный и встречается во всех списках самых частых паролей.&lt;/p&gt;
&lt;p&gt;Попрактикуйтесь — сможете ли вы подобрать пароль? pbkdf2_sha256 — это тип хеша и подписи, а 36000 — количество итераций. Справиться с задачей поможет hashcat, john the ripper или любой другой подборщик прообразов хешей.&lt;/p&gt;
&lt;h2&gt;Незапланированная уязвимость&lt;/h2&gt;
&lt;p&gt;Последняя уязвимость нашлась в функции добавлении продукта в холодильник. Здесь не проверялось, что вы являетесь владельцем добавляемого продукта. Можно было создать супер-холодильник, содержащий все продукты с номерами от 1 до 1000, а затем попросить API выдать рецепты, содержащие хотя бы какой-нибудь продукт из этого холодильника. Конечно, он находил и чужие рецепты, а вместе с ними выдавал и флаги, хранящиеся в описаниях этих рецептов.&lt;/p&gt;
&lt;h2&gt;Выводы&lt;/h2&gt;
&lt;p&gt;Никогда не используйте csv.DictReader/csv.DictWriter вместе с файлами, открытыми модулем codecs. В новых питонах открывайте CSV-файлы с помощью стандартной функции open, передавая ей аргументы encoding и newline.&lt;/p&gt;
&lt;h2&gt;Бонус для дочитавших до конца&lt;/h2&gt;
&lt;p&gt;Все исходники сервиса, докер-файлы, чекер для проверяющей системы и мой авторский эксплойт для первой уязвимости можно найти в &lt;a href="https://github.com/HackerDom/phdctf-2017"&gt;репозитории разработки&lt;/a&gt;, который мы открыли сразу после окончания соревнования.&lt;/p&gt;
</description>
</item>

<item>
<title>Заметка первая. Про необычное поведение юникода и CSV в питоне</title>
<guid isPermaLink="false">1</guid>
<link>https://andgein.ru/blog/all/1-unicode-and-csv-in-python/</link>
<pubDate>Sun, 14 May 2017 19:49:57 +0100</pubDate>
<author></author>
<comments>https://andgein.ru/blog/all/1-unicode-and-csv-in-python/</comments>
<description>
&lt;p&gt;&lt;i&gt;Посвящается Полине, которая рассказала мне про странное поведение своей питоновской программы и тем самым подсказала потрясающую идею для сервиса на CTF. &lt;/i&gt;&lt;/p&gt;
&lt;p&gt;Всё началось месяца полтора назад. Полина рассказала мне, как она со своим начальником долго искала проблему в скрипте, который всего-навсего читал большой юникодный файл и как-то его обрабатывал. Проблема заключалась в том, что в файле было, например, 4 миллиона строк, а объектов в итоге оказывалось на два больше — 4 000 002. Это при том, что файл обрабатывался стандартной для питона конструкцией:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;import codecs

with codecs.open(&amp;#039;file.txt&amp;#039;, &amp;#039;r&amp;#039;, &amp;#039;utf-8&amp;#039;) as f:
    for line in f:
        # Обрабатываем строчку line&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Я никак не мог за такое не ухватиться. Расспросил Полину подробно, выяснил, что ничего стороннего для работы с файлом не использовалось, и вообще конструкция была проста как топор. Откуда она берёт лишние строчки?&lt;/p&gt;
&lt;p&gt;Пришёл домой и сразу сел проверять:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;import codecs

with codecs.open(&amp;#039;file.txt&amp;#039;, &amp;#039;r&amp;#039;, &amp;#039;utf-8&amp;#039;) as f:
    print(len(f.readlines()))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Попробовал на нескольких файлах и всё, конечно, работает верно. Но Полина упоминала, что в её файле были какие-то &lt;i&gt;плохие символы&lt;/i&gt;, из-за которых и возникали лишние строчки. Я решил сгенерировать файл с кучей случайных юникодных символов:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;import codecs
import random

def generate_string():
    &amp;quot;&amp;quot;&amp;quot;
    Генерируем строчку случайных символов длины 100.
    Специально обходим символы с кодами 10 и 13, чтобы строка не содержала переводов строк
    &amp;quot;&amp;quot;&amp;quot;
    return &amp;#039;&amp;#039;.join(chr(random.randint(20, 10000)) for _ in range(100))

with codecs.open(&amp;#039;file.txt&amp;#039;, &amp;#039;w&amp;#039;, &amp;#039;utf-8&amp;#039;) as f:
    # Печатаем в файл 1000 строк
    for i in range(1000):
        f.write(generate_string() + &amp;#039;\n&amp;#039;)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;На всякий случай открываю файл в Фаре. Его немножко корёжит, но строчек он показывает ровно 1001, что абсолютно верно, ведь в файле 1000 переводов строк:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://andgein.ru/blog/pictures/2017-05-14_23-42-40.png" width="1365" height="717" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Запустил скрипт и удивился: он видит в файле 1076 строчек, а вовсе не 1000 и не 1001, как можно было бы предположить. Разобравшись и сгенерировав файл поменьше, нашёл тот самый символ, который всё портит. Им оказался символ с кодом 8233 — &lt;a href="http://www.fileformat.info/info/unicode/char/2029/index.htm"&gt;Paragraph Separator&lt;/a&gt;. Рядом с ним есть ещё один такой же символ — &lt;a href="http://www.fileformat.info/info/unicode/char/2028/index.htm"&gt;Line Separator&lt;/a&gt;, он имеет код 8232. И если подумать, то нет ничего странного, что с точки зрения модуля codecs в файле за этими символами начинаются новые строки.&lt;/p&gt;
&lt;p&gt;Кстати, новый open() в третьем питоне сам умеет читать файлы в UTF-8, модуль codecs можно не использовать. Но в данном случае он ведёт себя иначе:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;with open(&amp;#039;file.txt&amp;#039;, &amp;#039;r&amp;#039;, encoding=&amp;#039;utf-8&amp;#039;) as f:
    print(len(f.readlines()))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;На том же самом файле этот код выводет 1000. Такая разница в поведении уже кажется странной и неприятной, но давайте вернёмся к коду с модулем codecs и копнём поглубже.&lt;/p&gt;
&lt;p&gt;В юникоде есть символы, которые заставляют модуль codecs думать, что в файле началась новая строка. Это же отличное место для модифицированной &lt;a href="https://www.owasp.org/index.php/CRLF_Injection"&gt;CRLF-инъекции&lt;/a&gt;! Правда, в данном случае она будет скорее ParagraphSeparator-инъекцией, но сути это не меняет. Представим код, который сохраняет пользовательские строчки в файл, одну за другой. Чтобы злоумышленник не мог повлиять на структуру файла, из строчек выкидываются все символы ‘\r’ и ‘\n’. Раньше мы могли думать, что мы в безопасности, но теперь мы знаем — злоумышленник может использовать Paragraph Separator или Line Separator, чтобы повлиять на структуру файла.&lt;/p&gt;
&lt;h2&gt;Приглашаем на наш праздник CSV&lt;/h2&gt;
&lt;p&gt;Ну что ж, разобрались с Paragraph Separator. Теперь пора на основе этой &lt;i&gt;фичи&lt;/i&gt; вытворить что-нибудь совсем интересное. Наш следующий скрипт будет сохранять юникодные данные в CSV-файл, а другой — считывать эти данные строчка за строчкой. Включаем питоновский модуль &lt;a href="https://docs.python.org/3.5/library/csv.html"&gt;csv&lt;/a&gt; и экспериментируем:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;import codecs
import csv

with codecs.open(&amp;#039;file.txt&amp;#039;, &amp;#039;w&amp;#039;, &amp;#039;utf-8&amp;#039;) as f:
    writer = csv.DictWriter(f, fieldnames=[&amp;#039;first&amp;#039;, &amp;#039;second&amp;#039;])
    writer.writeheader()
    writer.writerow({&amp;#039;first&amp;#039;: &amp;#039;Hello world&amp;#039;, &amp;#039;second&amp;#039;: &amp;#039;Text with\n newline&amp;#039;})&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;В полученном файле оказывается три строки:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;first,second
Hello world,&amp;quot;Text with
 newline&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Умный DictWriter поставил кавычки в нужных местах, и теперь симметричный ему DictReader отлично считает эти данные:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;import codecs
import csv

with codecs.open(&amp;#039;file.txt&amp;#039;, &amp;#039;r&amp;#039;, &amp;#039;utf-8&amp;#039;) as f:
    reader = csv.DictReader(f)
    for row in reader:
        print(row)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Выводит то, что надо, отлично:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;{&amp;#039;first&amp;#039;: &amp;#039;Hello world&amp;#039;, &amp;#039;second&amp;#039;: &amp;#039;Text with\n newline&amp;#039;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Начинаем вставлять везде где ни попадя Paragraph Separator:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;writer.writerow({&amp;#039;first&amp;#039;: &amp;#039;Hello world&amp;#039;, &amp;#039;second&amp;#039;: &amp;#039;Text with&amp;#039; + chr(8233) + &amp;#039; paragraph separator&amp;#039;})&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Внезапно на выходе получаем файл без кавычек. DictWriter не считает этот символ чем-то опасным:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;first,second
Hello world,Text with  paragraph separator&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Ну что ж, натравим теперь на этот файл DictReader. В первый раз не мог поверить своим глазам, если честно:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;{&amp;#039;first&amp;#039;: &amp;#039;Hello world&amp;#039;, &amp;#039;second&amp;#039;: &amp;#039;Text with\u2029&amp;#039;}
{&amp;#039;first&amp;#039;: &amp;#039; paragraph separator&amp;#039;, &amp;#039;second&amp;#039;: None}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;То есть DictReader повёл себя несимметрично по отношению к DictWriter’у — сохраняли одну строчку, а получили две! Пользователь, влияющий на данные во втором поле первой строки, смог повлиять на первое поле во второй строке.&lt;/p&gt;
&lt;h2&gt;Кульминация&lt;/h2&gt;
&lt;p&gt;Собираем всё вместе и задаём вопрос знатокам питона:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;import codecs
import csv

data = {&amp;#039;first&amp;#039;: &amp;#039;...&amp;#039;, &amp;#039;second&amp;#039;: &amp;#039;...&amp;#039;}

with codecs.open(&amp;#039;file.txt&amp;#039;, &amp;#039;w&amp;#039;, &amp;#039;utf-8&amp;#039;) as f:
    writer = csv.DictWriter(f, fieldnames=data.keys())
    writer.writeheader()
    writer.writerow(data)

count = 0
with codecs.open(&amp;#039;file.txt&amp;#039;, &amp;#039;r&amp;#039;, &amp;#039;utf-8&amp;#039;) as f:
    reader = csv.DictReader(f)
    for row in reader:
        count += 1

print(count)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Каким может быть вывод переменной count в конце программы?&lt;/p&gt;
&lt;p&gt;Теперь-то мы знаем, что абсолютно любым — хоть 1, хоть 30, хоть 100. Например, инициализировав переменную data так:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;P_SEP = chr(8233)
data = {&amp;#039;first&amp;#039;: P_SEP * 10, &amp;#039;second&amp;#039;: P_SEP * 10}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;получим ответ 20.&lt;/p&gt;
&lt;h2&gt;Известно ли про это что-нибудь миру?&lt;/h2&gt;
&lt;p&gt;В документации к модулю csv написано: если вы используете csv.reader(), открывайте файл с опцией newline=’’:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://andgein.ru/blog/pictures/2017-05-15_00-49-29.png" width="1084" height="101" alt="" /&gt;
&lt;/div&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://andgein.ru/blog/pictures/2017-05-15_01-09-05.png" width="1058" height="130" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Но, во-первых, мы используем не простой csv.reader(), а более умный csv.DictReader(), а, во-вторых, модуль codecs не поддерживает опцию newline, мы просто не сможем открыть файл с ней.&lt;/p&gt;
&lt;p&gt;Интернет про эту проблему мне тоже ничего не рассказал. Но, может, я просто плохо искал?..&lt;/p&gt;
&lt;p&gt;&lt;i&gt;В &lt;a href="/blog/all/2-phdays-ctf-2017-service"&gt;следующей заметке&lt;/a&gt; я рассказал, как из этого получился отличный сервис для PHDays CTF.&lt;/i&gt;&lt;/p&gt;
</description>
</item>


</channel>
</rss>