[FRPythoneers] Python CGI & Security

Matt Gushee mgushee at havenrock.com
Tue Jul 9 22:59:57 MDT 2002


On Tue, Jul 09, 2002 at 09:06:52PM -0600, Evelyn Mitchell wrote:

> http://www.w3.org/Security/Faq/

Thanks. I may have seen this before, but I could use a refresher.
> 
> "#  Never, never, never  pass unchecked remote user input to a shell
> command."

Now, that much I know. Fortunately, my script doesn't use any shell
commands, so that helps.

> We'd be happy to look at the code, if you can show it to us.

Okay, here it is. I instinctively modularized it, by the way. I don't
know if that's wise for CGI or not, but I figured there might end up
being several form handlers with minor variations. Anyway, 'mailer.py'
is the library, and 'infomail.py' is the actual form handler.

------- mailer.py ---------------------------------------------------

#!/usr/bin/python

import cgi, StringIO, string, smtplib, sys, re, MimeWriter
from types import InstanceType

OK = 0

# Config

MAILSERVER     = "MAILSERVER"
ADDRESSES      = {}
SENDER         = "webform at itsamaritans.org"
SUBJECTS       = {}
SKIP_FIELDS    = ['email']
ERROR_TEMPLATE = "ERROR_TEMPLATE"
OK_TEMPLATE    = "OK_TEMPLATE"
FAIL_ON_EXTRA  = 1   # Abort if extraneous fields are found? 

class Mailer:
    # Note: this isn't 100% compliant w/ RFC822 but should be close enough
    email_expr = re.compile(r'^[^][()<>,;:\\"\s\000-\037]+@[^][()<>,;:\\"\s\000-\037]+$')
    def __init__(self, **kwargs):
        self.mailserver = kwargs.get('mailserver',MAILSERVER)
        self.addresses = kwargs.get('addresses',ADDRESSES)
        self.sender = kwargs.get('sender',SENDER)
        self.subjects = kwargs.get('subjects',SUBJECTS)
        self.skip_fields = kwargs.get('skip_fields',SKIP_FIELDS)
        self.error_template = kwargs.get('error_template',ERROR_TEMPLATE)
        self.ok_template = kwargs.get('ok_template',OK_TEMPLATE)
        self.fail_on_extra = kwargs.get('fail_on_extra',FAIL_ON_EXTRA)
        self.form = None
        self.valid_funcs = {}
        self.field_status = {}
        
    def validate_form(self):
        form = self.form
        if not form:
            raise IllegalStateError
        elif (type(form) is not InstanceType or
              form.__class__ is not cgi.FieldStorage):
            raise ValueError
        has_errors = 0
        for fname in form.keys():
            vfunc = self.valid_funcs.get(fname,None)
            if vfunc:
                (valid,error) = vfunc(form[fname].value)
                self.field_status[fname] = (valid,error)
                if not valid:
                    has_errors = 1
            elif self.fail_on_extra:
                raise ValueError, "Invalid field name: '%s'" % fname
        return not has_errors

    def _validate_email(self,address):
        if self.email_expr.match(string.strip(address)): return 1
        else: return 0
        
    def run(self):
        self.form = form = cgi.FieldStorage()
        field_status = self.field_status
        if self.validate_form():
            category = form['category'].value
            recipient = self.addresses[category]
            
            # Create output control

            out = StringIO.StringIO()
            mail = MimeWriter.MimeWriter(out)
            mail.addheader("Subject", self.subjects[category])

            # Set up the email

            mail.addheader("To", recipient)
            mail.addheader("From", self.sender)
    
            # Quick check
            assert self.mailserver != "MAILSERVER" and \
                   recipient != "ADDRESS"

            # Write email body
    
            fp = mail.startbody("text/plain")
            
            for fieldname in field_status.keys():
                if fieldname not in self.skip_fields:    
                    fp.write("---" + fieldname + ": \n")
                    fp.write(form[fieldname].value + "\n\n")

            # Send email    
            mail = smtplib.SMTP(self.mailserver)
            mail.sendmail(self.sender, recipient, out.getvalue())
        
            # Print confirmation
        
            print "Content-Type: text/html; charset=ISO-8859-1\n\n"
        
            print open(self.ok_template).read()
            
            OK = 1
                    
        else:
            err_dict = {}
            for k in self.valid_funcs.keys():
                err_dict[k] = ""
            err_report = '    <ul class="ErrorMessage">\n'
            for fname in field_status.keys():
                if field_status[fname][0]:
                    err_dict[fname] = form[fname].value
                else:
                    err_dict[fname] = ""
                    err_report = "%s      <li>%s</li>\n" % (err_report,
                                                            field_status[fname][1])
            err_report = "%s    </ul>\n" % err_report
            err_dict['err_report'] = err_report

            print "Content-Type: text/html; charset=ISO-8859-1\n\n"

            print open(self.error_template).read() % err_dict

            OK = 1
            sys.exit()


------------ infomail.py ----------------------------------------------

#!/usr/bin/env python

import mailer, cgi, string
from types import InstanceType

# Config

MAILSERVER     = "mail.itsamaritans.org"
#MAILSERVER     = "mail.hypermall.net"
ADDRESSES      = {'volunteer':'volunteer at itsamaritans.org',
                  'services':'services at itsamaritans.org',
                  'general':'info at itsamaritans.org'}
#ADDRESSES      = {'volunteer':'mgushee at havenrock.com',
#                  'services':'mgushee at havenrock.com',
#                  'general':'mgushee at havenrock.com'}
SUBJECTS       = {'volunteer':"Volunteer Inquiry",
                  'services':"Service Inquiry",
                  'general':"Information Request"}
SKIP_FIELDS    = ['category']
#ERROR_TEMPLATE = "/var/www/templates/badmail_html"
ERROR_TEMPLATE = "/home/mjmikelson/templates/badmail_html"
#OK_TEMPLATE    = "/var/www/templates/mailok_html"
OK_TEMPLATE    = "/home/mjmikelson/templates/mailok_html"

# maximum characters for text fields
NAME_LEN = 128       
EMAIL_LEN = 128       
PHONE_LEN = 32
QUESTION_LEN = 4096


class InfoRequestMailer(mailer.Mailer):

    def __init__(self):
        mailer.Mailer.__init__(self,mailserver=MAILSERVER,
                               addresses=ADDRESSES,subjects=SUBJECTS,
                               skip_fields=SKIP_FIELDS,
                               error_template=ERROR_TEMPLATE,
                               ok_template=OK_TEMPLATE)
        self.valid_funcs = {'category': self.validate_category,
                           'name': self.validate_name,
                           'email': self.validate_email,
                           'phone': self.validate_phone,
                           'contactpref': self.validate_contactpref,
                           'question': self.validate_question}
    def validate_category(self,input):
        if input in ('volunteer','services','general'):
            return (1,None)
        else:
            return (0,"Please use the form.")
    def validate_name(self, input):
        if not input:
            return (0,"You must enter your name.")
        elif len(input) > NAME_LEN:
            return (0,"Your name must have 128 or fewer characters.")
        else:
            return (1, None)
    def validate_email(self, input):
        if not input:
            return (0,"You must enter a valid e-mail address.")
        elif len(input) > EMAIL_LEN:
            return (0,"Your e-mail address must have 128 or fewer characters.")
        elif self._validate_email(input):
            return (1, None)
        else:
            return (0,"You must enter a valid e-mail address.")
    def validate_phone(self, input):
        if input and (len(input) < 7 or len(input) > PHONE_LEN): 
            return (0,"You appear to have entered an invalid phone number.")
        else:
            return (1, None)
    def validate_contactpref(self,input):
        if input in ('email','phone'):
            return (1,None)
        else:
            return (0,"Please use the form.")
    def validate_question(self, input):
        if input and len(input) > QUESTION_LEN:
            return (0,"Your question may only have 4096 characters (about 600 words).")
        else:
            return (1, None)

InfoRequestMailer().run()

-- 
Matt Gushee
Englewood, Colorado, USA
mgushee at havenrock.com
http://www.havenrock.com/



More information about the FRPythoneers mailing list