Вы не представляете, как меня достал антиспамный плагин в моем текущем форуме на движке PunBB :-(. Он ловит действительно много спама, но он мне складывает этот спам в карантин пачками по 150 сообщений несколько раз в день, и мне надо это все глазками просматривать и ручками удалять. Причем эти сообщения он старательно рисует полностью, а многие из них сами занимают по нескольку экранов. А не глядя удалять спам я не могу, потому что никакая эвристика (там работает Akismet) не гарантирует отсутствие ham'а. И тут очередная беда: когда ошибочно задержанное письмо объявляешь чистым, плагин не сообщает об этом в Akismet, и тот не тренирует свой анализатор. Саша Кошелев не даст соврать — его посты я доставал из карантина чуть не пару месяцев кряду.

Это, как ничто другое, сподвигло меня взять себя в руки и вернуться к Cicero. Ему, по большому счета, только антиспама и не хватало до релиза.

Bzr-репозиторий http://softwaremaniacs.org/code/cicero/
Работающий форум http://softwaremaniacs.org/forum/

Антиспамная цепочка

Сначала я приведу кусочек кода, а потом разъясню идею за ним.

def spam_validators():
  for module_name in settings.ANTISPAM_PLUGINS:
    yield __import__(module_name, {}, {}, [''])

def validate(request, article, is_new_topic):
  status = None
  for module in spam_validators():
    result = module.validate(request, article, is_new_topic)
    if result in ['clean', 'spam']:
      return result
    if result is not None:
      status = result
  return status or 'clean'

Анализ спама в Cicero делается цепочкой фильтров. Каждый фильтр — это функция, которая получает на вход созданную пользователем статью и сопровождающие параметры запроса. Эта функция смотрит на переданную информацию и может вернуть четыре варианта ответа:

Если фильтр в цепочке точно уверен в своем заключении (возвращает "clean" или "spam"), то обработка прерывается и этот ответ считается окончательным. При других вариантах ответа обработка продолжается.

У этого нюанса есть свои причины. Автоматическая проверка на спам по сути своей операция с нечетким ответом. Однако эта нечеткость бывает разная. Например если статья идет от имени давно зарегистрированного пользователя, то она практически точно не спамная ("практически" — это на случай, если человека заставляли запостить спам, угрожая проигрыванием песен Ф. Киркорова). Аналогично, есть проверки, показывающие, что пост — практически точно спам. А вот анализу контента сервисами вроде Akismet'а доверять так без оглядки уже нельзя. Такое разделение очень полезно, потому что более четкие проверки, прекращая обработку поста, позволяют разгрузить карантинную очередь. А это, как я убедился (см. начало статьи) — очень важно в первую очередь для здоровья модератора.


Фото с drinkinghabits.com

Далее, если ни один из фильтров цепочки не дал никакого определенного ответа, то новой статье присваивается статус "clean". Я решил, что перестраховка (автоматический "suspect") здесь будет работать плохо. Во-первых, мне кажется, что пролезшего по ошибке спама, который модераторам придется вычищать, будет меньше, чем нормальных статей, которые пришлось бы пропускать. А во-вторых, это позволит форуму, хоть и со спамом, но функционировать, когда модераторы не захотят уделять ему много времени, лежа под зонтиком на белом песке на удаленном карибском пляже, потягивая мохито из заиндевелого бокальчика.

Настройка settinngs.ANTISPAM_PLUGINS содержит строчки питоновских путей к модулям, в лучших традициях Джанго. Каждый модуль должен содержать функцию validate(request, article, is_new_topic), которая и вызывается для проверки. Все это теоретически позволяет сказать, что Cicero позволяет подключать сторонние фильтры, но по-честному этого недостаточно. Потому что помимо информации, которая пришла извне, фильтру нужно минимум еще уметь влиять на содержимое формы во время рисования. А этого пока никак сделать нельзя. Поэтому можно пока считать, что архитектура допускает только предустановленный набор.

Фильтры

Написание самих фильтров оказалось довольно несложным делом. Их у меня сейчас три.

"Honeypots" — основан на статье Неда Батчелдера, но сделан сильно проще. Фактически я вставляю в форму постинга поле с name="email" и скрываю его от взора людей CSS'ом. Если оно приходит заполненным — это был спам-бот. Я не стал зашифровывать названия полей, потому что это убивает браузерное автозаполнение, да и нужно будет, только когда боты будут учитывать особенности Cicero. Тогда я буду просто смотреть, что они делают, и дописывать фильтры по факту.

"Whitelist" — помечает посты от известных пользователей. Все пользователи с OpenID, которые получают свой профиль в Cicero, изначально имеют нейтральный статус относительно спамерства. Этот статус можно изменить на "спамер" и "не спамер". Именно им фильтр и руководствуется, выдавая посту соответствующий статус (таким образом он не только whitelist, но еще и blacklist, но название сложилось исторически). Изменение статуса делается либо вручную в админке, либо если модератор явно помечает пост пользователя как спам, либо наоборот помечает как не спам в карантинной очереди.

Наконец последним фильтром стоит "Akismet". Передача статьи на анализ на сервер Akismet'а делается с помощью библиотеки Дэвида Линча, выбранной наугад из двух питоньих библиотек на сайте. Ее, правда, пришлось немножко поправить от небольших багов. Хотя, там, в общем-то, библиотека особенно и не нужна, потому что весь протокол — это один вызов функции через XML-RPC. А черную работа — перекладывание параметров запроса из джанговского request'а в параметры функции — пришлось все равно делать вручную :-).

Ну и кроме того, я не поленился все же сделать отсылку на сайт Akismet'а уведомлений об исправлении его решений — непойманного спама и пойманного не спама.

Логика view

Вот где мне пришлось помучиться — это в логике уровня view, где POST из формы превращается в статью, которую надо проверить на спам. Главная сложность в том, что у меня постинг статьи может происходить за два запроса. Это происходит, если человек, не будучи залогиненным во время отправки формы, указывает в качестве подписи свой OpenID. Тогда его статья сохраняется в базе от имени гостевого пользователя, но после этого человек переправляется на авторизацию на свой OpenID-сервер, в результате чего может вернуться с подтверждением успешной авторизации обратно. В этот момент сохраненная статья приписывается уже ему лично.

Проверка на спам полностью происходит в обоих этих случаях. Интересный эффект получается, если OpenID человека объявлен спамерским. Тогда, если его статья не поймана по другим признакам, она уже сохраняется в форуме. И только если он подтвердит, кто он такой, уже будет удалена как явный спам :-)

Сеть белых списков

Эта часть статьи на самом деле важнее всего остального. Мне хочется дать толчок идее, которую я впервые вычитал у Саймона ВиллисонаSocial Whitelisting.

Поскольку OpenID не дает никаких гарантий по степени доверия пользователю, правильным поведением считается пользователю не доверять до тех пор, пока он как-то это самое доверие не заработает. Тогда он вносится в некий белый список, и сайт предоставляет ему какие-то дополнительные привилегии. Также можно утверждать, что чаще всего "вменяемость" пользователя не зависит от того, какими сайтами он пользуется. И вот отсюда вытекает идея о том, что сайты могут делиться друг с другом информацией о том, какие их пользователи являются вменяемыми. Самое главное для этого — универсальность имени пользователя — предоставляется как раз OpenID.

Важная особенность такой системы — ее децентрализованность. Если бы был некий единый реестр "чистых" OpenID, то он не относился бы к пользователям индивидуально, и спамеры могли бы явно нацелиться на то, чтобы понять, как он работает, выяснить необходимый минимум усилий для попадания туда OpenID-идентификатора, потом насоздавать их там большое количество и просто пользоваться этим пулом для спама везде. Децентрализованная система позволяет хозяину каждого конкретного форума самостоятельно выбирать несколько источников, чьему мнению о чистоте пользователей он доверяет. И каждый из этих источников — хозяин, следящий за своим хозяйством — будет справляться с этим лучше, и быстрее исправлять ошибки, чем любая глобальная система.

Публикация белых списков

Так вот главная цель этого поста — призвать рунетовских хозяев форумов и блогов, где есть вход по OpenID, организовать у себя такой белый список. Особенно хорошо было бы, если бы такую фичу поддержали в определенных популярных блог-движках. А как именно это можно сделать, поделюсь на примере реализации в Cicero.

Есть отдельная view'ха, которая просто напросто выдает список всех OpenID, которые явно помечены как не спамеры. Причем ответ она умеет выдавать в трех форматах:

Делать это именно так — никакое не требование, потому что идея еще не распространена, и формат не устоялся. Однако минимально, видимо, надо уметь по крайней мере простой текст. Как, например, сделано у того же Саймона.

Мой код определяет формат по тому, что клиент запрашивает в заголовке Accept, используя для его парсинга библиотечку mimeparse:

def openid_whitelist(request):

  # получение списка URL'ов
  openids = (p.openid for p in Profile.objects.filter(spamer=False) if p.openid)

  # определение предпочтительного формата из списка поддерживаемых
  from cicero.utils.mimeparse import best_match
  MIMETYPES = ['application/xml', 'text/xml', 'application/json', 'text/plain']
  accept = request.META.get('HTTP_ACCEPT')
  if accept:
    mimetype = best_match(MIMETYPES, accept)
  else:
    mimetype = 'text/plain'  # text/plain -- по умолчанию

  # выдача ответов разных типов
  if mimetype.endswith('/xml'):
    try:
      import xml.etree.ElementTree as ET
    except ImportError:
      import elementtree.ElementTree as ET
    root = ET.Element('whitelist')
    for openid in openids:
      ET.SubElement(root, 'openid').text = openid
    xml = ET.ElementTree(root)
    response = HttpResponse(mimetype=mimetype)
    xml.write(response, encoding='utf-8')
    return response
  if mimetype == 'application/json':
    from django.utils import simplejson
    response = HttpResponse(mimetype=mimetype)
    simplejson.dump(list(openids), response)
    return response
  if mimetype == 'text/plain':
    return HttpResponse((o + '\n' for o in openids), mimetype=mimetype)

  # если запрошен неизвестный формат - выдать ошибку
  response = HttpResponse('Can accept only: %s' % ', '.join(MIMETYPES))
  response.status_code = 406
  return response

На мой, пока небогатый, whitelist можно посмотреть. В большинстве браузеров он, скорее всего, выдастся в XML-ном виде.

Вторая важная вещь — это собственно публикация. В вашем форуме или блоге обязательно должна быть где-нибудь в футере или сайдбаре ссылка типа "OpenID whitelist". Наверное было бы еще хорошо, чтобы кто-нибудь придумал для нее уникальную иконку, но пока можно пользоваться универсальной иконкой "Share".

Чтение белых списков

Cicero для чтения белых списков имеет две таблички: WhitelistSource, где хранятся URL'а источников, и CleanOpenID, где хранятся сами OpenID. С последней как раз и сверяется плагин "whitelist", если OpenID человека не имеет собственного статуса в самом Cicero.

class WhitelistSource(models.Model):
  url = models.URLField()

  class Admin:
    pass

  def __unicode__(self):
    return self.url

class CleanOpenID(models.Model):
  openid = models.CharField(max_length=200, db_index=True)
  source = models.ForeignKey(WhitelistSource)

  class Meta:
    unique_together = [('openid', 'source')]
    ordering = ['openid']
    verbose_name = 'Clean OpenID'
    verbose_name_plural = 'Clean OpenIDs'

  class Admin:
    list_display = ['openid', 'source']
    list_filter = ['source']

  def __unicode__(self):
    return self.openid

Обновляются списки "тупо по крону"™. Однако для вящего удобства тех, кто захочет Cicero использовать, я оформил скрипт обновления в виде джанговской команды, вызов которой в кроне выглядит примерно так:

1    6    *    *    *    /home/maniac/sm_org/manage.py update_whitelist

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


Что же касается Cicero, то до его публикации осталось совсем немного: нормально сверстать и импортировать посты из текущего форума. Остальные наполеоновские планы уже будут реализовываться потом.

Комментарии: 28

  1. otakuneko.ya.ru

    Я ваш блог всегда с живым интересом читаю :) Спасибо, что пишете про Django.

  2. dobrych

    Иван, мегареспект тебе за углубленое раскрытие темы!

    Хооршие примеры кода и постановка задачи. Я думаю что whitelisting должен обязательно шариться. Это нужно как-то продвигать, только сначала немного стандартизировать.

  3. Иван Сагалаев

    Это нужно как-то продвигать, только сначала немного стандартизировать.

    А это только одновременно происходит. Если мы все сделаем это одинаково, это и станет стандартом. Я предложил некий минимум. Если разовьется, можно уже дальше думать про отдельный mime-тип, про rel="whitelist" для ссылки и т.д.

  4. Дмитрий Т.

    Заwhiteliste'нным я бы тоже доверял до определённого предела - затроянить их могут.

    Кстати есть централизованная система которая как-то вроде тоже борется со спамом (особых деталей я не нашёл, а вычитал про неё на блоге Москалюка, который её прикрутил уже к standalone блогу): http://disqus.com.

    P.S. openid через http://openid-provider.appspot.com/ не прошёл тут почему-то.

  5. david-m.livejournal.com

    Интересно, нет ли каких-нибудь подводных камней в таком вот открытом публиковании «белых» OpenID-ов? С одной стороны, на первый взгляд это не более опасно чем публикация IP, к тому же все эти адреса и так из форума вытягиваются при желании. С другой стороны, мы получаем готовый и машиночитаемый список адресов (с большой вероятностью — адресов блогов) гарантированно реальных людей, которых можно, скажем, проспамить.

  6. Иван Сагалаев

    Заwhiteliste'нным я бы тоже доверял до определённого предела - затроянить их могут.

    Децентрализованная система как раз это позволяет: можно доверять человеку, только если он есть в листах у нескольких разных источников.

    С другой стороны, мы получаем готовый и машиночитаемый список адресов

    Он, как Вы правильно заметили, и так уже есть, правда не в таком удобном виде. Собственно, если человек пользуется своим OpenID, он должен понимать, что этот URL он именно публикует. Плюс, люди свои блоги в большинстве своем не скрывают. А если захотят скрыть, то OpenID-страница может быть совершенно отвлеченной, не обязательно блогом.

  7. Александр Соловьёв

    Особенно хорошо было бы, если бы такую фичу поддержали в определенных популярных блог-движках.

    Хорошая идея. :-) Не обещаю, что это будет в ближайшие дни, но мне как минимум интересно это сделать. :-)

    Все это теоретически позволяет сказать, что Cicero позволяет подключать сторонние фильтры, но по-честному этого недостаточно. Потому что помимо информации, которая пришла извне, фильтру нужно минимум еще уметь влиять на содержимое формы во время рисования. А этого пока никак сделать нельзя.

    Хмм... Требовать ещё метод get_field, для получения поля для формы? И в принципе тогда всё, никаких проблем... Хотя есть вариант, что ему придётся форму менять, тогда надо метод update_form, но это не очень честно и безопасно в плане совместной работы нескольких плагинов.

  8. david-m.livejournal.com

    Плюсы-то подхода и так понятны. Интереснее придумать минусы.

  9. Иван Сагалаев

    Хмм... Требовать ещё метод get_field, для получения поля для формы?

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

    Плюсы-то подхода и так понятны. Интереснее придумать минусы.

    Я понимаю :-). В прицнипе можно подумать про то, что списки очень большими могут получаться, и выдавать их будет дорого. Но эта проблема тоже, по идее, решается через conditional get, например.

  10. Дмитрий Дзема

    Фактически я вставляю в форму постинга поле с name="email" и скрываю его от взора людей CSS'ом. Если оно приходит заполненным — это был спам-бот

    Мы тоже так сделали в одном проекте и возникает проблема с файрфоксом. Он автоматом этот email заполнит, а мы юзера нафиг пошлем. Поэтому нужен именно тот вариант, что описан в статье, когда поля обфускейтятся. Правда и автозаполнение браузером тогда ломается, как вы заметили.

  11. Иван Сагалаев

    Разве автоматом подставляет? По-моему, только если начать его заполнять руками... По крайней мере в Cicero прямо сейчас такой проблемы я не замечю.

  12. Алексей

    Красиво. Только а почему не как в почтовых фильтрах, оценка-значение «уверенности», скажем, от 0.0 до 1.0 (в связи открываются возможности давать плагинам-фильтрам разный вес, задавать планку и т.п.), а только тринарное ['clean', 'suspect', 'spam']?

  13. Taras

    А чем RBL-листы не подходят?

  14. ARSolog

    Соглашусь с Давидом Мзареулян.
    Если whitelist будут общедоступными,
    можем столкнуться со спамерством по ним.
    На опенид страничках пользователей могут располагаться
    машиночитаемые личные данные типа мыла аськи и прочее.

  15. Иван Сагалаев

    Красиво. Только а почему не как в почтовых фильтрах, оценка-значение «уверенности», скажем, от 0.0 до 1.0 (в связи открываются возможности давать плагинам-фильтрам разный вес, задавать планку и т.п.), а только тринарное ['clean', 'suspect', 'spam']?

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

    А чем RBL-листы не подходят?

    Они немного не про то. Да, тоже про спам, но там блокировка по доменам и IP-адресам. RBL не сможет сказать, что юзер с OpenID "username1.ya.ru" — спамер, а "username2.ya.ru" — нет.

    На опенид страничках пользователей могут располагаться машиночитаемые личные данные типа мыла аськи и прочее.

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

  16. bw

    фильтру нужно минимум еще уметь влиять на содержимое формы во время рисования

    А вот это уже интересно. Как ты себе представляешь такую шичу? Фильтры будут согласовывать между собой какую информацию они хотят получить от автора? Т.е. некоторое согласование конечно же должно быть, так что бы не получилось, что если каждому из трех зарегистрированных фильтров потребуется от пользователя электронный почтовый адрес, то на форму выводилось три поля EMail. Группировать поля по имени? А у нас есть право на такое вольное интерпретирование хотелок фильтра? К тому же некоторые поля будут взаимоисключающими, например если пользователель идентифицирует себя по OpenID, то спрашивать у него, например, логин и пароль, будет лишним.

    ..bw

  17. Иван Сагалаев

    В том и дело, что пока я этого себе не представляю :-). Все гипотетические вопросы этого гипотетического API заключается как раз в том, что они пока не реальные, а воображаемые. Придумывать воображаемые ответы на воображаемые вопросы, наверное, интересно, но пользы в этом мало...

  18. Дмитрий Дзема

    Разве автоматом подставляет? По-моему, только если начать его заполнять руками...

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

  19. Иван Сагалаев

    Я подумал тут, что в любом случае от проблемы можно избавиться атрибутом autocomplete="off".

  20. vestel

    видать у человека, который тестил, плагин какой-то стоял

    У меня были такие траблы при использовании Firefox с Google Toolbar

  21. gezakht

    Нужна централизованная база черных идентификаторов, обновляемая из нескольких независимых источников для 100% валидации аккаунтов.

  22. Денис Зайцев

    Интересно, а ведь технология OpenID вполне может стать той основой "паспортизации всея интернета", которой нас пугают больш.. силовики :)
    Вот уж, правда, благими намерениями...

  23. Иван Сагалаев

    Принципиальная разница, что OpenID может выдать сам себе кто угодно.

  24. Денис Зайцев

    Пока да :)

    Но ради благородной идеи борьбы с терроризмом/криминалом/спамом (нужное подчеркнуть) можно ведь ситуацию и изменить. Законодательно...

    Однако, пожалуй хватит страшилок в стиле Джорджа Оруэлла ;)

  25. vankovalenko

    Кстати децентрализованный whitelisting - дело довольно распространенное. Например, те же DNSBL для smtp серверов. Или drbl для них же.

  26. Алексей Новиков

    Недавно скинули мне ссылку на эту статью. Радует, что не одинок в своих изысканиях.

    Недавно я выпустил плагин и сервис Parasite Eliminator. Борется с ручным спамом в блогах, пока только на Wordpress.

    И вот, как раз, собираюсь вводить туда фишку с белыми списками людей, которым блоггер доверяет. Скажем, доверяет Петя Мише и Васе. Вводит их URL, а Вася и Миша открывают ему свои белые списки.

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

    Теперь думаю, как эту систему могут "отравить"?

  27. [...] меня там конвейер. А Акисмет делает меньшую часть работы, на самом [...]

  28. Озадачен написанием конвейера из автомодераторов для проверки спама в объектах моделей django.И в связи с этим вопрос, какие сервисы вы использовали для автоматической проверки на спам?Немаловажно также есть ли готовые python библиотеки или клиенты, для работы с такими удалёнными антиспам сервисами.Поискал и нашёл пока следующее:1. Akismet 2. Defensio 3. TypedPad AntispamЕсть ли что добавить к этому списку? Есть ли российские сервисы?Akismet думаю что активно используют, но для проектов с малой монетизацией и не блогов не всегда подходит.У Defensio имеет бесплатный план для стартапов, но в общем услуги платные.TypedSpam похоже идеален, но есть мысли что база спама у них маленькая.Темы которые здесь подымались:http://softwaremaniacs.org/blog/2008/04/28/antispam-and-social-whitelisting/http://softwaremaniacs.org/forum/django/14693/

Добавить комментарий