[or-cvs] r12045: Various security things: * Use uradom() for security-related (weather/trunk)

pde at seul.org pde at seul.org
Fri Oct 19 08:54:47 UTC 2007


Author: pde
Date: 2007-10-19 04:54:47 -0400 (Fri, 19 Oct 2007)
New Revision: 12045

Added:
   weather/trunk/TorCtl
Modified:
   weather/trunk/README
   weather/trunk/TODO
   weather/trunk/config.py
   weather/trunk/poll.py
   weather/trunk/weather.py
Log:
Various security things:

* Use uradom() for security-related randomness
* Speak _no_ differently when an existing subscription is re-requested 
* Limit the number of unconfirmed requests started by each IP for a number of hours
* Prevent ports > 65535 being sent to openssl

Plus:

* improved commentary
* parse_subscriptions() is nicer



Modified: weather/trunk/README
===================================================================
--- weather/trunk/README	2007-10-19 02:31:43 UTC (rev 12044)
+++ weather/trunk/README	2007-10-19 08:54:47 UTC (rev 12045)
@@ -20,6 +20,6 @@
 
 Weather stores its records in a set of gdbm databases: requests.gdbm,
 subscriptions.gdbm, unsubscriptions.gdbm, and failures.gdbm.  For real usage,
-it absolutely essential to backup subscriptions.gdbm properly, and
+it is absolutely essential to backup subscriptions.gdbm properly, and
 unsubscriptions.gdbm is pretty important too (though the code could be modified
 to recover from its loss).

Modified: weather/trunk/TODO
===================================================================
--- weather/trunk/TODO	2007-10-19 02:31:43 UTC (rev 12044)
+++ weather/trunk/TODO	2007-10-19 08:54:47 UTC (rev 12045)
@@ -1,15 +1,18 @@
-* Security audit
-
 * Fix TorCtl.Connection.close()
 
 * What happens if the openssl command gets a go-slow DOS attack in response?
 
-* Might someone attack weather with a huge number of spurious subscription requests?  Should we have
-  exponential backoff or similar?
-
 * figure out how to disable HTTP logging through web.py, and should decide what
   diagnostic logging makes sense (currently, there is a debug variable in each
   of weather.py and poll.py; turning these on currently produces a lot of
   semi-informative chatter on stdout and turns the web server into an oracle
   for whether address x has already subscribed to alerts about node y, which is
   of course unsatisfactory) 
+
+* either disable the reason parameter in send_failure_email() or replace it
+  with something that's less likely to leak anything sensitive (either about
+  Tor's config, through control port errors, or about a third party's response
+  to attempted openssl connections)
+
+* replace the openssl s_client call with a lookup to tor's v3 status Running?
+  consensus?

Added: weather/trunk/TorCtl
===================================================================
--- weather/trunk/TorCtl	                        (rev 0)
+++ weather/trunk/TorCtl	2007-10-19 08:54:47 UTC (rev 12045)
@@ -0,0 +1 @@
+link ../TorCtl/
\ No newline at end of file


Property changes on: weather/trunk/TorCtl
___________________________________________________________________
Name: svn:special
   + *

Modified: weather/trunk/config.py
===================================================================
--- weather/trunk/config.py	2007-10-19 02:31:43 UTC (rev 12044)
+++ weather/trunk/config.py	2007-10-19 08:54:47 UTC (rev 12045)
@@ -2,7 +2,7 @@
 
 authenticator = ""  # customise this
 
-#URLbase = "http://weather.torpoject.org"
+#URLbase = "http://weather.torproject.org"
 URLbase = "http://ip-adress:port"
 
 weather_email = "no-reply at torproject.org"

Modified: weather/trunk/poll.py
===================================================================
--- weather/trunk/poll.py	2007-10-19 02:31:43 UTC (rev 12044)
+++ weather/trunk/poll.py	2007-10-19 08:54:47 UTC (rev 12045)
@@ -23,7 +23,6 @@
   "Check to see if various tor nodes respond to SSL hanshakes"
   def __init__(self, control_host = "127.0.0.1", control_port = 9051):
     "Keep the connection to the control port lying around"
-    # need to know this hasn't gone down!
     self.control_host = control_host
     self.control_port = control_port
     self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
@@ -55,8 +54,8 @@
     ip,port = info[string].split()[6:8]
     # throw exceptions like confetti if this isn't legit
     socket.inet_aton(ip)
-    # port 0 is not kosher
-    assert int(port) > 0
+    # ensure port is a kosher string
+    assert 0 < int(port) < 65536
 
     if debug: print "contacting node at %s:%s" % (ip,port)
 

Modified: weather/trunk/weather.py
===================================================================
--- weather/trunk/weather.py	2007-10-19 02:31:43 UTC (rev 12044)
+++ weather/trunk/weather.py	2007-10-19 08:54:47 UTC (rev 12045)
@@ -1,7 +1,6 @@
 #!/usr/bin/env python2.5
+import os
 import web
-import string
-import socket
 import DNS
 import re
 import random
@@ -10,10 +9,12 @@
 import time
 import threading
 import signal # does this help with keyboard interrupts?
+import base64
 
 from config import URLbase, weather_email
 
 debug = 0
+dummy_testing = 0
 
 DNS.ParseResolvConf()
 
@@ -28,7 +29,6 @@
 
 # This is a single lock for all the gdbm write rights, to ensure that
 # different web.py threads aren't trying to write at the same time.
-# poll.py has its own gdbm objects, but they only read from these databases.
 
   gdbm_lock = threading.RLock()
 
@@ -45,6 +45,12 @@
   for s in unsubscriptions.keys():
     print s, unsubscriptions[s]
 
+  antispam_lock = threading.RLock()
+  antispam = {}      # a dict mapping IP to the number of recent unanswered requests allowed
+                     # from that IP
+  antispam_min = 2
+  antispam_max = 10
+
 # these may or may not be better than storing pickles with gdbm
 
 class DatabaseError(Exception):
@@ -53,27 +59,18 @@
 def parse_subscriptions(node, subs):
   "Turn a string in the db back into a list of pairs"
   words = subs[node].split()
-  if (len(words) % 2 != 0):
+  try:
+    return [ (words[i], words[i+1]) for i in xrange(0, len(words), 2) ]
+  except IndexError:
     raise DatabaseError, words
 
-  results = []
-  while True:
-    try:
-      auth = words.pop()
-    except IndexError:
-      break
-    email = words.pop()
-    results.append((email, auth))
-
-  return results
-
 def delete_sub(pair, sub, node):
   "Craziness to delete pair from a string in the subscriptions db"
   # regexps probably aren't easily made safe here
   words = sub[node].split()
   if (len(words) % 2 != 0):
     raise DatabaseError, words
-  for n in range(len(words) / 2):
+  for n in xrange(len(words) / 2):
     if pair[0] == words[n*2] and pair[1] == words[n*2 + 1]:
       sub[node] = " ".join(words[:n*2] + words[n*2 + 2:])
       break
@@ -81,11 +78,9 @@
     raise DatabaseError, pair
   sub.sync()
       
-random.seed()
 def randstring():
   "Produce a random alphanumeric string for authentication"
-  # This gives us a 190.5 bit random space
-  return "".join([random.choice(chars) for x in xrange(32)])
+  return base64.urlsafe_b64encode(os.urandom(18))[:-1]
 
 subscribe_text = \
 """Dear human, this is the Tor Weather Report system.  
@@ -119,12 +114,18 @@
       print "That doesn't look like a proper Tor node ID."
       return True
 
+    if not self.allowed_to_subscribe(web.ctx.ip):
+      print "Sorry, too many recent unconfirmed subscriptions from your IP address."
+      return True
+
     if not self.already_subscribed(i.email, node_cleaned):
       self.send_confirmation_email(i.email, node_cleaned)
     elif debug:
       print "Sorry, I'm not subscribing you twice."
+    else:
+      # Leak no information about who is subscribed
+      print "Thankyou for using Tor Weather.  A confirmation request has been sent to", i.email + "."
 
-  
   # node ids are 40 digit hexidecimal numbers
   node_okay = re.compile("(0x)?[a-fA-F0-9]{40}\Z")
 
@@ -134,6 +135,24 @@
     else:
       return False
 
+  random.seed()
+  def allowed_to_subscribe(self,ip):
+    "An antispam measure!"
+    antispam_lock.acquire()
+    if antispam.has_key(ip):
+      if antispam[ip] == 0:
+        antispam_lock.release()
+        return False
+      else:
+        antispam[ip] -= 1
+        antispam_lock.release()
+        return True
+    else:
+      # okay this is silly but leaks very slightly less information
+      antispam[ip] = random.randrange(antispam_min,antispam_max)
+      antispam_lock.release()
+      return True
+
   def already_subscribed(self, address, node):
     gdbm_lock.acquire()
 
@@ -155,6 +174,11 @@
     gdbm_lock.acquire()
     requests[authstring] = address + " " + node
     gdbm_lock.release()
+
+    if dummy_testing:
+      print "gotcha"
+      return True
+
     #def f(s):
     #  requests[authstring] = s
     #gdbm_lock.lock(f, address + " " + node)
@@ -269,6 +293,14 @@
     del(requests[authstring])
     subscriptions.sync()
     gdbm_lock.release()
+    # okay now slacken antispam watch
+    antispam_lock.acquire()
+    if antispam.has_key(web.ctx.ip):
+      antispam[web.ctx.ip] += 1
+      if antispam[web.ctx.ip] >= antispam_max:
+        del antispam[web.ctx.ip]
+    antispam_lock.release()
+
     
 class unsubscribe:
   def GET(self,authstring):
@@ -288,11 +320,27 @@
     del (unsubscriptions[authstring])
     gdbm_lock.release()
 
+class AntispamRelaxer(threading.Thread):
+  "Prevent long term accretion of antispam counts."
+  timescale = 24 * 3600          # sleep for up to a day
+  def run(self):
+    while True:
+      time.sleep(random.randrange(0,self.timescale))
+      antispam_lock.acquire()
+      for ip in antispam.keys():
+        antispam[ip] += 1
+        if antispam[ip] == antispam_max:
+          del antispam[ip]
+      antispam_lock.release()
+
 def main():
   from poll import WeatherPoller
   weather_reports = WeatherPoller(subscriptions, gdbm_lock)
   weather_reports.start()                 # this starts another thread
 
+  relaxant = AntispamRelaxer()
+  relaxant.start()
+
   web.run(urls, globals())
 
 



More information about the tor-commits mailing list