CSV из CRM: как отправлять подозрительные поля на проверку
Правила выгрузки заявок в CSV: недоверенные поля, отдельный журнал исключений и проверка политики на искусственных данных без запуска таблиц.
Почему CSV из CRM требует отдельной проверки
Перед передачей выгрузки менеджеру разделите машинный обмен и просмотр в электронной таблице. Поля, введённые пользователем, могут оказаться формулами для программы, которая откроет файл. Подозрительные строки лучше задержать для разбора по согласованной политике, сохранив исходные значения в защищённом хранилище. Заключение полей в кавычки само по себе не делает формулы безопасными.
OWASP описывает CSV Injection как попадание недоверенных данных в CSV с последующей интерпретацией содержимого электронной таблицей. В частности, ячейка, начинающаяся с =, может быть прочитана как формула. CWE-1236 отдельно описывает эту слабость и ограничения способов её устранения.[1][2]
Недоверенный текст может оказаться в названии компании, комментарии к обращению или поле, перенесённом из другого сервиса. Данные проходят через доверенную CRM, но происхождение текста от этого не меняется. Право скачать файл тоже не означает, что все его значения безопасны для следующей программы.
Ниже предложена политика для собственного обработчика экспорта. Это не описание проверенной встроенной функции 24go.site, не универсальная защита всех таблиц и не разрешение собирать чужие контакты. Пример работает только с искусственными записями. Он проверяет решения обработчика, не запускает Excel или Calc и не обращается к CRM.
Сначала договоритесь, кто откроет файл
У выгрузки должен быть конкретный потребитель. Файл для переноса между системами требует сохранения значений по контракту. Файл для просмотра человеком требует проверки поведения выбранной программы. Названия кнопок, описание формата и маршрут хранения должны отражать эту разницу.
| Назначение | Что согласовать | Что считать ошибкой |
|---|---|---|
| Машинный перенос | Схему, кодировку, разделитель, обработку пустых значений | Незаметное изменение телефона или комментария |
| Просмотр менеджером | Программу, версию, настройки импорта, правила исключений | Открытие недоверенного текста как формулы |
| Разбор исключений | Ответственного, доступ, срок хранения, способ просмотра | Перенос исходных подозрительных значений в обычный CSV |
Для человеческого отчёта начните с минимального набора колонок. Если менеджеру достаточно номера заявки, статуса и даты, не добавляйте полный текст переписки. Уменьшение состава полей сокращает объём недоверенного текста и раскрываемых сведений; оно не заменяет проверку оставшихся значений.
Процесс передачи заявок связан с интеграцией 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] Поэтому проверка первого открытия недостаточна для политики, которая полагается на преобразование ячеек. Если необходимого приложения нет, этот этап остаётся непроверенным, а не автоматически успешным.
Карантин также требует эксплуатационной проверки: получатель видит число исключений, ответственный может найти исходную заявку, повторный экспорт не теряет её и не снимает ограничение самопроизвольно. Изменение статуса заявки не должно означать автоматическое признание комментария безопасным.
Как внедрять без потери заявок
Сначала включите наблюдение в тестовом контуре, согласуйте причины задержки с владельцем процесса и убедитесь, что записи не исчезают из исходной системы. Решение о включении производственной блокировки принимается по конкретным тестам и требованиям команды, не по наличию одного успешного скрипта.
- Опишите колонки и разрешённые типы; отдельно укажите правила для пустой строки, отсутствующего значения и отрицательных чисел.
- Назначьте каждой выгрузке потребителя и запретите незаметно менять режим просмотра на машинный импорт.
- Сохраните примеры разрешения и задержки, включая запятые, кавычки, пробелы, управляющие и полноширинные символы.
- Проверьте отказ всего задания при нарушении схемы. Не отправляйте частично записанный файл как успешный результат.
- Согласуйте защищённый разбор исключений и способ повторного экспорта после решения человека.
- После обновления библиотеки, приложения или колонок повторите затронутые проверки и сохраните условия запуска.
Начните с одного своего отчёта: перечислите недоверенные поля, найдите его фактического получателя и согласуйте поведение при первой задержанной строке. Если это пока неизвестно, экспортный договор ещё не готов. Обсуждать использование 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