Permalink
Browse files

first pass at forms and sessions

  • Loading branch information...
1 parent 4b1a565 commit 28d68fee062f1aaa4f2a694e30f68fdb9f983e3e @andymckay committed Nov 15, 2012
@@ -0,0 +1,3 @@
+# Try to configure the reporters.
+from django_paranoia.configure import config
+config()
@@ -0,0 +1,27 @@
+import logging
+
+from django.utils.importlib import import_module
+
+from signals import warning
+
+log = logging.getLogger('paranoia')
+
+
+def config(reporters=None, *args, **kw):
+ if not reporters:
+ try:
+ from django.conf import settings
+ reporters = getattr(settings, 'DJANGO_PARANOIA_REPORTERS', [])
+ except ImportError:
+ # This can occur when running the tests, because at this time
+ # the settings module isn't created. TODO: fix this.
+ return
+
+ for reporter in reporters:
+ try:
+ to = import_module(reporter).report
+ except ImportError:
+ log.error('Failed to register the reporter: %s' % reporter)
+ continue
+
+ warning.connect(to, dispatch_uid='django-paranoia-%s' % reporter)
No changes.
View
@@ -0,0 +1,15 @@
+from django.utils.translation import ugettext as _
+
+EXTRA_FIELDS = 'extra-form-fields'
+SQL_INJECTION = 'sql-injection-attempt'
+XSS = 'xss-attempt'
+SESSION_CHANGED = 'session-changed'
+
+trans = {
+ EXTRA_FIELDS: _('Attempt to process form with extra values'),
+ SQL_INJECTION: _('Data looks that like SQL injection attempt'),
+ XSS: _('Data that looks like XSS attempt'),
+ SESSION_CHANGED: _('Session data changed'),
+}
+
+__all__ = [EXTRA_FIELDS, SQL_INJECTION, XSS]
View
@@ -0,0 +1,35 @@
+from django.forms import Form, ModelForm
+
+from flags import EXTRA_FIELDS, trans
+from signals import warning
+
+# Work in progress.
+# We aren't trying to be definitive here yet. Just spot stupid things.
+
+class Paranoid(object):
+
+ def __init__(self, data=None, files=None, **kwargs):
+ super(Paranoid, self).__init__(data=data, files=files, **kwargs)
+ data = data or {}
+ extra = [k for k in data if k not in self.fields]
+ if extra:
+ klass = self.__class__
+ msg = (u'%s: %s in %s' %
+ (trans[EXTRA_FIELDS], extra, klass.__name__))
+ warning.send(sender=klass, flag=EXTRA_FIELDS,
+ message=msg, values=extra)
+
+ def clean(self):
+ super(Paranoid, self).clean()
+ for k, v in self.cleaned_data.values():
+ # Spot SQL injection attempts.
+ # Spot XSS attempt.
+ pass
+
+
+class ParanoidForm(Paranoid, Form):
+ pass
+
+
+class ParanoidModelForm(Paranoid, ModelForm):
+ pass
No changes.
@@ -0,0 +1,7 @@
+import logging
+
+log = logging.getLogger('paranoia')
+
+
+def report(signal, message=None, flag=None, sender=None, values=None):
+ log.warning(message)
@@ -0,0 +1,64 @@
+from django.conf import settings
+from django.contrib.sessions.middleware import SessionMiddleware
+from django.contrib.sessions.backends.cache import SessionStore as Base
+from django.core.cache import cache
+
+from signals import warning
+from flags import trans, SESSION_CHANGED
+
+KEY_PREFIX = 'django_paranoid.sessions:'
+DATA_PREFIX = '%sdata' % KEY_PREFIX
+META_KEYS = ['REMOTE_ADDR']
+
+
+class SessionStore(Base):
+
+
+ def __init__(self, request=None, session_key=None):
+ self._cache = cache
+ self.request = request
+ super(SessionStore, self).__init__(session_key)
+
+ @property
+ def cache_key(self):
+ return KEY_PREFIX + self._get_or_create_session_key()
+
+ def load(self):
+ return super(SessionStore, self).load()
+
+ def save(self, must_create=False):
+ data = self._get_session(no_load=must_create)
+ data.setdefault(DATA_PREFIX, {})
+ for k in META_KEYS:
+ data[DATA_PREFIX]['meta:%s' % k] = self.request.META.get(k, '')
+ return super(SessionStore, self).save(must_create=must_create)
+
+ def create(self):
+ # Having Django wildly create new sessions is bizarre. Let's
+ # instead ensure they are unique. Maybe.
+ pass
+
+ def request_data(self):
+ return self._get_session(no_load=False)[DATA_PREFIX]
+
+ def check_request_data(self, request):
+ data = self._get_session()
+ for k in META_KEYS:
+ saved = data[DATA_PREFIX]['meta:%s' % k]
+ current = request.META.get(k, '')
+ if saved and saved != current:
+ values = [saved, current]
+ msg = msg = (u'%s: %s' % (trans[SESSION_CHANGED], values))
+ warning.send(sender=self, flag=SESSION_CHANGED,
+ message=msg, values=values)
+
+
+class ParanoidSessionMiddleware(SessionMiddleware):
+
+ def process_request(self, request):
+ if settings.SESSION_ENGINE != 'django_paranoia.sessions':
+ raise ValueError('SESSION_ENGINE must be django_paranoia.sessions')
+
+ session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None)
+ request.session = SessionStore(request=request,
+ session_key=session_key)
@@ -0,0 +1,3 @@
+from django.dispatch import Signal
+
+warning = Signal(providing_args=['type', 'message', 'values'])
No changes.
No changes.
@@ -0,0 +1,143 @@
+import uuid
+
+from django.conf import settings
+
+minimal = {
+ 'DATABASES': {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': 'mydatabase'
+ }
+ },
+ 'CACHES': {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
+ }
+ },
+ 'INSTALLED_APPS': ['django.contrib.sessions'],
+ 'DJANGO_PARANOIA_REPORTERS': ['django_paranoia.reporters.log'],
+}
+
+if not settings.configured:
+ settings.configure(**minimal)
+
+from django import forms
+from django.db import models
+from django.test import TestCase
+from django.test.client import RequestFactory
+from django.utils.importlib import import_module
+
+import mock
+from nose.tools import eq_
+from django_paranoia.configure import config
+from django_paranoia.forms import ParanoidForm, ParanoidModelForm
+from django_paranoia.sessions import SessionStore
+from django_paranoia.signals import warning
+
+
+class SimpleForm(ParanoidForm):
+ yes = forms.BooleanField()
+
+
+class SimpleModel(models.Model):
+ yes = models.BooleanField()
+
+
+class SimpleModelForm(ParanoidModelForm):
+
+ class Meta:
+ model = SimpleModel
+
+
+class TestForms(TestCase):
+
+ def result(self, *args, **kwargs):
+ self.called.append((args, kwargs))
+
+ def setUp(self):
+ self.called = []
+ self.connect = warning.connect(self.result)
+
+ def test_fine(self):
+ SimpleForm()
+ SimpleForm({'yes': True})
+ assert not self.called
+
+ def test_extra(self):
+ SimpleForm({'no': 'wat'})
+ assert self.called
+
+ def test_multiple(self):
+ SimpleForm({'no': 'wat', 'yes': True, 'sql': 'aargh'})
+ res = self.called[0][1]
+ eq_(res['values'], ['sql', 'no'])
+
+ def test_model_fine(self):
+ SimpleModelForm()
+ SimpleModelForm({'yes': True})
+ assert not self.called
+
+ def test_model_extra(self):
+ SimpleModelForm({'no': 'wat'})
+ assert self.called
+
+
+@mock.patch('django_paranoia.configure.warning')
+class TestSetup(TestCase):
+ log = 'django_paranoia.reporters.log'
+
+ def test_setup_fails(self, warning):
+ config([self.log, 'foo'])
+ eq_(len(warning.connect.call_args_list), 1)
+ eq_(warning.connect.call_args[1]['dispatch_uid'],
+ 'django-paranoia-%s' % self.log)
+
+
+class TestLog(TestCase):
+ # Not sure what to test here.
+ pass
+
+
+class TestSession(TestCase):
+
+ def setUp(self):
+ self.session = None
+ self.uid = 'some:uid'
+ self.called = []
+ self.connect = warning.connect(self.result)
+
+ def result(self, *args, **kwargs):
+ self.called.append((args, kwargs))
+
+ def request(self, **kwargs):
+ req = RequestFactory().get('/')
+ req.META.update(**kwargs)
+ return req
+
+ def get(self, request=None):
+ session = SessionStore(request=request or self.request(),
+ session_key=self.uid)
+ self.uid = session._session_key
+ return session
+
+ def save(self):
+ self.session.save()
+
+ def test_basic(self):
+ self.session = self.get()
+ self.session['foo'] = 'bar'
+ self.save()
+ eq_(self.get().load()['foo'], 'bar')
+
+ def test_request(self):
+ self.get().save()
+ res = self.get()
+ eq_(res.request_data(), {'meta:REMOTE_ADDR': '127.0.0.1'})
+ assert not self.called
+
+ def test_request_changed(self):
+ ses = self.get()
+ ses.save()
+ req = self.request(REMOTE_ADDR='192.168.1.1')
+ ses.check_request_data(request=req)
+ assert self.called
No changes.
View
@@ -0,0 +1,3 @@
+Django>=1.4
+nose
+mock
View
No changes.

0 comments on commit 28d68fe

Please sign in to comment.