CRMбезопасностьэкспорт

CSV из CRM: как отправлять подозрительные поля на проверку

9 мин чтения

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

Почему CSV из CRM требует отдельной проверки

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

OWASP описывает CSV Injection как попадание недоверенных данных в CSV с последующей интерпретацией содержимого электронной таблицей. В частности, ячейка, начинающаяся с =, может быть прочитана как формула. CWE-1236 отдельно описывает эту слабость и ограничения способов её устранения.[1][2]

Недоверенный текст может оказаться в названии компании, комментарии к обращению или поле, перенесённом из другого сервиса. Данные проходят через доверенную CRM, но происхождение текста от этого не меняется. Право скачать файл тоже не означает, что все его значения безопасны для следующей программы.

Ниже предложена политика для собственного обработчика экспорта. Это не описание проверенной встроенной функции 24go.site, не универсальная защита всех таблиц и не разрешение собирать чужие контакты. Пример работает только с искусственными записями. Он проверяет решения обработчика, не запускает Excel или Calc и не обращается к CRM.

Сначала договоритесь, кто откроет файл

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

НазначениеЧто согласоватьЧто считать ошибкой
Машинный переносСхему, кодировку, разделитель, обработку пустых значенийНезаметное изменение телефона или комментария
Просмотр менеджеромПрограмму, версию, настройки импорта, правила исключенийОткрытие недоверенного текста как формулы
Разбор исключенийОтветственного, доступ, срок хранения, способ просмотраПеренос исходных подозрительных значений в обычный CSV
Не называйте машинный CSV безопасным для открытия двойным щелчком. У него другой договор с получателем. Если требуется точная передача текста, можно выбрать JSON с явно описанной схемой, но сам выбор JSON не управляет тем, что получатель сделает с данными дальше. После преобразования в таблицу проверка понадобится на новой границе.

Для человеческого отчёта начните с минимального набора колонок. Если менеджеру достаточно номера заявки, статуса и даты, не добавляйте полный текст переписки. Уменьшение состава полей сокращает объём недоверенного текста и раскрываемых сведений; оно не заменяет проверку оставшихся значений.

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

Разделите сериализацию и политику формул

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

Документация Python описывает csv.writer, параметры разделителя и кавычек, а также открытие файлов с newline=''. По умолчанию csv.reader возвращает строки без автоматического преобразования типов. Это поведение парсера Python, не обещание о том, как тот же файл откроет электронная таблица.[3]

Не собирайте строку через ','.join(...). Запятая, кавычка или перенос строки в комментарии должны остаться частью своего поля. В примере используется csv.writer с кавычками для всех полей, затем csv.reader проверяет обратное чтение. Политика подозрительных префиксов применяется до записи, а не подменяется параметром QUOTE_ALL.

OWASP перечисляет среди рискованных начал =, +, -, @, табуляцию, возврат каретки, перевод строки и некоторые полноширинные варианты символов. Там же указано, что разделители и кавычки могут изменить границу ячейки при неправильной обработке.[1] Список помогает составить тесты, но не доказывает полноту конкретного детектора.

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

Что означает карантин всей строки

В предложенной политике строка с подозрительным полем не попадает в человеческий CSV. Вместо неё в отдельный журнал исключений записываются идентификатор, название колонки и код причины. Исходное поле остаётся в системе с ограниченным доступом. Обработчик не пытается автоматически исправить значение и не удаляет заявку.

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

СитуацияРешение примераДальнейшее действие
Обычный текстРазрешить строкуПроверить структуру CSV
Формульный префикс после начальных пробеловЗадержать всю строкуРазобрать поле в защищённом интерфейсе
Табуляция или перенос строки в любом местеЗадержать всю строкуПодтвердить необходимость многострочного текста
Неизвестный статус или неверная схемаОстановить обработку с ошибкойИсправить контракт, не выдавать успешный файл
Запятая и кавычки внутри обычного текстаСохранить через CSV-библиотекуСверить значение после обратного чтения
В реальном процессе полезно показывать два числа: сколько строк включено и сколько задержано. Получатель должен знать, что отчёт неполон. Не называйте файл полной выгрузкой, если часть строк осталась на проверке. Для ответственного за исключения нужен отдельный список работ с контролем доступа.

Сам журнал не должен становиться вторым каналом проблемы. Не складывайте в его CSV исходный подозрительный комментарий и не отправляйте весь текст в уведомление. В примере JSON-журнал содержит только искусственный ID, имя поля и причину. Просмотр исходных данных потребует интерфейса, который выводит их как текст и не интерпретирует как разметку или формулы.

Воспроизводимый тест без электронной таблицы

Сохраните код как csv_policy_check.py и выполните python3 csv_policy_check.py. Нужен Python 3 со стандартной библиотекой. Программа создаёт данные только в памяти, проверяет разрешённые строки и журнал исключений. Она не пишет выгрузку на диск и не открывает таблицы. Значение =1+1 служит безопасной строкой для теста правила, вычисления не происходит.

import csv
import io
import json
import unicodedata

PREFIXES = set("=+-@=+-@")
FIELDS = ("id", "status", "note")
STATUSES = {"new", "qualified"}

def reason(value):
    if any(ch in value for ch in "\t\r\n"):
        return "control_separator"
    probe = value.lstrip()
    if probe and unicodedata.category(probe[0]) == "Cf":
        return "leading_format_character"
    if probe and probe[0] in PREFIXES:
        return "formula_prefix"
    return None

def export_for_review(rows):
    accepted = []
    held = []
    for row in rows:
        if set(row) != set(FIELDS):
            raise ValueError("schema")
        if not all(isinstance(row[k], str) for k in FIELDS):
            raise ValueError("types")
        if not row["id"].isascii() or not row["id"].isdigit():
            raise ValueError("id")
        if row["status"] not in STATUSES:
            raise ValueError("status")
        issue = reason(row["note"])
        if issue:
            held.append({"id": row["id"], "field": "note", "reason": issue})
        else:
            accepted.append(row)
    out = io.StringIO(newline="")
    writer = csv.DictWriter(out, fieldnames=FIELDS, quoting=csv.QUOTE_ALL)
    writer.writeheader()
    writer.writerows(accepted)
    result = out.getvalue()
    restored = list(csv.DictReader(io.StringIO(result, newline="")))
    assert restored == accepted
    return result, held

rows = [
    {"id": "1", "status": "new", "note": "Call tomorrow"},
    {"id": "2", "status": "new", "note": '=1+1'},
    {"id": "3", "status": "qualified", "note": 'Said "yes", call later'},
    {"id": "4", "status": "new", "note": "  +44123"},
    {"id": "5", "status": "new", "note": "line\nbreak"},
    {"id": "6", "status": "new", "note": "=1+1"},
    {"id": "7", "status": "new", "note": "\u200b=1+1"},
]
text, held = export_for_review(rows)
assert [r["id"] for r in csv.DictReader(io.StringIO(text))] == ["1", "3"]
assert [r["id"] for r in held] == ["2", "4", "5", "6", "7"]
for value in ("=1+1", "+1", "-1", "@name", "=1", "+1", "-1", "@name"):
    assert reason(value) == "formula_prefix"
assert reason("  =1+1") == "formula_prefix"
assert reason("\tplain") == "control_separator"
assert reason("plain\rtext") == "control_separator"
for bad in (
    {"id": "8", "status": "unknown", "note": "plain"},
    {"id": "x", "status": "new", "note": "plain"},
    {"id": "8", "status": "new", "note": None},
):
    try:
        export_for_review([bad])
    except ValueError:
        pass
    else:
        raise AssertionError("invalid row accepted")
print(json.dumps({"accepted_ids": ["1", "3"], "held": held}, sort_keys=True))

Ожидаются разрешённые ID 1 и 3; остальные тестовые записи попадут в журнал с причинами. Проверки также требуют отказа при неизвестном статусе, нецифровом ID и значении None. Ошибка даёт traceback или AssertionError; такой запуск нельзя считать подтверждением политики.

Детектор сознательно задерживает любой перенос строки в комментарии и начальный форматирующий символ категории Cf после пробелов. Это собственные дополнительные ограничения примера. Они не объявлены требованиями CSV или полным описанием поведения Unicode в таблицах. После изменения набора колонок проверку надо расширить на каждое новое недоверенное поле.

Какие проверки остаются за пределами Python

Обратное чтение доказывает сохранение двух разрешённых тестовых записей в данном диалекте CSV. Оно не доказывает, что все версии Excel, Calc и другие программы одинаково обработают файл. OWASP прямо предупреждает об отсутствии универсальной стратегии для всех приложений и последующих потребителей.[1]

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

Затем сохраните и повторно откройте файл тем же маршрутом, которым пойдёт менеджер. OWASP отмечает, что Excel может убирать кавычки или экранирующие символы при сохранении и повторном открытии.[1] Поэтому проверка первого открытия недостаточна для политики, которая полагается на преобразование ячеек. Если необходимого приложения нет, этот этап остаётся непроверенным, а не автоматически успешным.

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

Как внедрять без потери заявок

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

  1. Опишите колонки и разрешённые типы; отдельно укажите правила для пустой строки, отсутствующего значения и отрицательных чисел.
  2. Назначьте каждой выгрузке потребителя и запретите незаметно менять режим просмотра на машинный импорт.
  3. Сохраните примеры разрешения и задержки, включая запятые, кавычки, пробелы, управляющие и полноширинные символы.
  4. Проверьте отказ всего задания при нарушении схемы. Не отправляйте частично записанный файл как успешный результат.
  5. Согласуйте защищённый разбор исключений и способ повторного экспорта после решения человека.
  6. После обновления библиотеки, приложения или колонок повторите затронутые проверки и сохраните условия запуска.
Для больших выгрузок запись в память из примера может не подходить. Производственный обработчик должен учитывать ограничения размера, временное хранилище, права доступа и атомарную выдачу готового файла. Это задачи реализации, не свойства приведённой функции. Не начинайте скачивание пользователю до проверки условий, при которых вы обещаете полный результат.

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

Источники

[1] OWASP, CSV Injection: https://owasp.org/www-community/attacks/CSV_Injection

[2] MITRE, CWE-1236: https://cwe.mitre.org/data/definitions/1236.html

[3] Python, csv: https://docs.python.org/3/library/csv.html