[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[or-cvs] r12045: Various security things: * Use uradom() for security-related (weather/trunk)
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@xxxxxxxxxxxxxx"
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())