6

Python安全编码指南 | WooYun知识库

 6 years ago
source link:
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Python安全编码指南

0x00 前言


from:http://sector.ca/Portals/17/Presentations15/SecTor_Branca.pdf

这个pdf中深入Python的核心库进行分析,并且探讨了在两年的安全代码审查过程中,一些被认为是最关键的问题,最后也提出了一些解决方案和缓解的方法。我自己也在验证探究过程中添油加醋了一点,如有错误还请指出哈。

下面一张图表示他们的方法论:

探究的场景为:

  • 输入的数据是"未知"的类型和大小
  • 使用RFC规范构建Libraries
  • 数据在没有经过适当的验证就被处理了
  • 逻辑被更改为是独立于操作系统的

0x01 Date and time —> time, datetime, os


time

asctime

#!python
import time 
initial_struct_time = [tm for tm in time.localtime()]       

# Example on how time object will cause an overflow 
# Same for: Year, Month, Day, minutes, seconds  
invalid_time = (2**63)      

# change ‘Hours' to a value bigger than 32bit/64bit limit   
initial_struct_time[3] = invalid_time       

overflow_time = time.asctime(initial_struct_time)

这里面asctime()函数是将一个tuple或者是struct_time表示的时间形式转换成类似于Sun Jun 20 23:21:05 1993的形式,可以time.asctime(time.localtime())验证一下。对time.struct_time(tm_year=2015, tm_mon=11, tm_mday=7, tm_hour=20, tm_min=58, tm_sec=57, tm_wday=5, tm_yday=311, tm_isdst=0)中每一个键值设置invalid_time可造成溢出错误。

  • Python 2.6.x中报错为OverflowError: long int too large to convert to int

  • Python 2.7.x中报错为

    • OverflowError: Python int too large to convert to C long
    • OverflowError: signed integer is greater than maximum

自己在64位Ubuntu Python2.7.6也测试了一下,输出结果为:

[-] hour:
    [+] OverflowError begins at 31: signed integer is greater than maximum
    [+] OverflowError begins at 63: Python int too large to convert to C long
...

gmtime

#!python
import time 
print time.gmtime(-2**64)   
print time.gmtime(2**63)

time.gmtime()为将秒数转化为struct_time格式,它会基于time_t平台进行检验,如上代码中将秒数扩大进行测试时会产生报错ValueError: timestamp out of range for platform time_t。如果数值在-2^63到-2^56之间或者2^55到2^62之间又会引发另一种报错ValueError: (84, 'Value too large to be stored in data type')。我自己的测试结果输出如下:

[-] 2 power:
    [+] ValueError begins at 56: (75, 'Value too large for defined data type')
    [+] ValueError begins at 63: timestamp out of range for platform time_t
[-] -2 power:
    [+] ValueError begins at 56: (75, 'Value too large for defined data type')
    [+] ValueError begins at 64: timestamp out of range for platform time_t

os

#!python
import os   
TESTFILE = 'temp.bin'       

validtime = 2**55   
os.utime(TESTFILE,(-2147483648, validtime)) 
stinfo = os.stat(TESTFILE)  
print(stinfo)       

invalidtime = 2**63 
os.utime(TESTFILE,(-2147483648, invalidtime))   
stinfo = os.stat(TESTFILE)  
print(stinfo)

这里的os.utime(path, times)是设置对应文件的access和modified时间,时间以(atime, mtime)元组的形式传入,代码中将modified time设置过大也会产生报错。

  • Python 2.6.x中报错为OverflowError: long int too large to convert to int

  • Python 2.7.x, Python 3.1中报错为OverflowError: Python int too large to convert to C long

如果我们将其中的modified time设置为2^55,ls后会有:

#!bash
$ ls -la temp.bin   
-rw-r--r-- 1 user01 user01 5 13 Jun 1141709097 temp.bin
$ stat temp.bin 
A:"Oct 10 16:31:45 2015"    
M:"Jun 13 01:26:08 1141709097"  
C: ”Oct 10 16:31:42 2015"

在某些操作系统上如果我们将值设为2^56,将会有以下输出(也有造成系统崩溃和数据丢失的风险):

#!bash
$ ls -la temp.bin   
Segmentation fault: 11  
$ stat temp.bin 
A:"Oct 10 16:32:50 2015"    
M:"Dec 31 19:00:00 1969"    
C:"Oct 10 16:32:50 2015"

Modules通常没有对无效输入进行检查或者测试。例如,对于64位的操作系统,最大数可以达到2^63-1,但是在不同的情况下使用数值会造成不同的错误,任何超出有效边界的数字都会造成溢出,所以要对有效的数据进行检验。

0x02 Numbers —> ctypes, xrange, len, decimal

ctype

ctypes是Python的一个外部库,提供和C语言兼容的数据类型,具体可见官方文档

测试代码:

#!python
import ctypes       

#32-bit test with max 32bit integer 2147483647  
ctypes.c_char * int(2147483647)     

#32-bit test with max 32bit integer 2147483647 + 1  
ctypes.c_char * int(2147483648)     

#64-bit test with max 64bit integer 9223372036854775807 
ctypes.c_char * int(9223372036854775807)        

#64-bit test with max 64bit integer 9223372036854775807 + 1 
ctypes.c_char * int(9223372036854775808)

举个栗子,可以在64位的操作系统上造成溢出:

#!python
>>> ctypes.c_char * int(9223372036854775808)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
OverflowError: cannot fit 'long' into an index-sized integer

Python ctypes 可调用的数据类型有:

问题在于:

  • ctypes对内存大小没有限制
  • 没有对溢出进行检查

所以,在32位和64位操作系统上都可以造成溢出,解决方案就是也要对数据的有效性和溢出进行检查。

xrange()

演示代码:

#!python
valid = (2 ** 63) -1    
invalid = 2 ** 63       

for n in xrange(invalid):   
    print n 

报错为:OverflowError: Python int too large to convert to C long。虽然这种行为是“故意”的和在预期之内的,但在这种情况下依旧没有进行检查而导致数字溢出,这是因为xrange使用Plain Integer Objects而无法接受任意长度的对象。解决方法就是使用Python的long integer object,这样就可以使用任意长度的数字了,限制条件则变为操作系统内存的大小了。

len()

演示代码:

#!python
valid = (2**63)-1   
invalid = 2**63     

class A(object):    
    def __len__(self):  
        return invalid      

print len(A())

这里也会报错:OverflowError: long int too large to convert to int。因为len()函数没有对对象的长度进行检查,也没有使用python int objects(使用了就会没有限制),当对象可能包含一个“.length”属性的时候,就有可能造成溢出错误。解决办法同样也是使用python int objects。

Decimal

#!python
from decimal import Decimal 
try:    
    # DECIMAL '1172837167.27'   
    x = Decimal("1172837136.0800")  
    # FLOAT '1172837167.27' 
    y = 1172837136.0800 
    if y > x:   
        print("ERROR: FLOAT seems comparable with DECIMAL") 
    else:   
        print("ERROR: FLOAT seems comparable with DECIMAL") 
except Exception as e:  
    print("OK: FLOAT is NOT comparable with DECIMAL")

以上代码是将Decimal实例和浮点值进行比较,在不同Python版本中如果无法比较则用except捕获异常,输出情况为:

  • Python 2.6.5, 2.7.4, 2.7.10中输出ERROR: FLOAT seems comparable with DECIMAL (WRONG)

  • Python 3.1.2中输出OK: FLOAT is NOT comparable with DECIMAL (CORRECT)

Type Comparsion

#!python
try:    
    # STRING 1234567890 
    x = "1234567890"    
    # FLOAT '1172837167.27' 
    y = 1172837136.0800 
    if y > x:   
        print("ERROR: FLOAT seems comparable with STRING")  
    else:   
        print("ERROR: FLOAT seems comparable with STRING")  
except Exception as e:  
    print("OK: FLOAT is NOT comparable with STRING")

以上代码是将字符串和浮点值进行比较,在不同Python版本中如果无法比较则用except捕获异常,输出情况为:

  • Python 2.6.5, 2.7.4, 2.7.10中输出ERROR: FLOAT seems comparable with STRING (WRONG)

  • Python 3.1.2中输出OK: FLOAT is NOT comparable with STRING (CORRECT)

在使用同一种类型的对象进行比较之后,Python内置的比较函数就不会进行检验。但在以上两个代码例子当中Python并不知道该如何把STRING和FLOAT进行比较,就会直接返回一个FALSE而不是产生一个Error。同样的问题也发生于在将DECIMAL和FLOATS时。解决方案就是使用强类型(strong type)检测和数据验证。

0x03 Strings —> input, eval, codecs, os, ctypes


eval()

#!python
import os   
try:    
    # Linux/Unix    
    eval("__import__('os').system('clear')", {})    
    # Windows   
    #eval("__import__('os').system(cls')", {})  
    print "Module OS loaded by eval"    
except Exception as e:  
    print repr(e)

关于eval()函数,Python中eval带来的潜在风险这篇文章也有提到过,使用__import__导入os,再结合eval()就可以执行命令了。只要用户加载了解释器就可以没有限制地执行任何命令

input()

#!python
Secret = "42"       

value = input("Answer to everything is ? ")     

print "The answer to everything is %s" % (value,)

在以上的代码中input()会接受原始输入,如何这里用户传入一个dir()再结合print,就会执行dir()的功能返回一个对象的大部分属性:

#!python
Answer to everything is ? dir() 
The answer to everything is 
[‘Secret’, '__builtins__', '__doc__', '__file__', '__name__',
'__package__']

我在这里看到了有一个Secret对象,然后借助原来程序的功能就可以得到该值:

#!python
Answer to everything is ? Secret    
The answer to everything is 42

codecs

#!python
import codecs   
import io       

b = b'\x41\xF5\x42\x43\xF4' 
print("Correct-String %r") % ((repr(b.decode('utf8', 'replace'))))      

with open('temp.bin', 'wb') as fout:    
    fout.write(b)   
with codecs.open('temp.bin', encoding='utf8', errors='replace') as fin:
    print("CODECS-String %r") % (repr(fin.read()))  
with io.open('temp.bin', 'rt', encoding='utf8', errors='replace') as fin:
    print("IO-String %r") % (repr(fin.read()))

以上的代码将\x41\xF5\x42\x43\xF4以二进制的形式写入文件,再分别用codecsio模块进行读取,编码形式为utf-8,对\xF5\xF4不能编码的设置errors='replace',编码成为\\ufffd,最后结果如下:

Correct-String —> "u'A\\ufffdBC\\ufffd'"
CODECS-String —> "u'A\\ufffdBC'" (WRONG)
IO-String —> "u'A\\ufffdBC\\ufffd'" (OK)

codecs在读取\x41\xF5\x42\x43\xF4这个字符串的时候,它期望接收到包含4个字节的序列,而且因为在读入\xF4的时候它还会再等待其他3个字节,而没有进行编码,结果就是得到的字符串有一段被删除了。更好且安全的方法就是使用os模块,读取整个数据流,然后进行解码处理。解决方案就是使用io模块或者对字符串进行识别和确认来检测畸形字符。

os

#!python
import os   
os.environ['a=b'] = 'c' 
try:    
    os.environ.clear()  
    print("PASS => os.environ.clear removed variable 'a=b'")    
except: 
    print("FAIL => os.environ.clear removed variable 'a=b'")    
    raise

在不同的平台上,环境变量名的名称和语法都是基于不同的规则。但Python并遵守同样的逻辑,它尽量使用一种普遍的接口来兼容大多数的操作系统。这种重视兼容性大于安全的选择,使得用于环境变量的逻辑存在缺陷。

#!bash
$ env -i =value python -c 'import pprint, os;
pprint.pprint(os.environ); del os.environ[""]'      

environ({'': 'value'})  
Traceback (most recent call last):  
    File "<string>", line 1, in <module>    
    File "Lib/os.py", line 662, in __delitem__  
        self.unsetenv(encodedkey)   
OSError: [Errno 22] Invalid argument

上面的代码使用env -i以一个空的环境开始,再设置一个键为空值为value的环境变量,使用python打印出来再删除。这样就可以定义一个键为空的环境变量了,也可以设置在键名中包含"=",但是会无法移除它:

#!bash
$ env -i python -c 'import pprint, posix, os;
os.environ["a="]="1"; print(os.environ); posix.unsetenv("a=")'      

environ({'a=': ‘1'})    
Traceback (most recent call last):  
    File "<string>", line 1, in <module>    
OSError: [Errno 22] Invalid argument

根据不同的版本,Python也会有不同的反应:

  • Python 2.6 —> NO ERRORS,允许无效操作!
  • PYTHON 2.7 —> OSError: [Errno 22] Invalid argument
  • PYTHON 3.1 —> NO ERRORS,允许无效操作!

解决方案是对基础设施和操作系统进行检测,检测和环境变量相关的键值对,阻止一些对操作系统为空或者无效键值对的使用。

ctypes

#!python
buffer=ctypes.create_string_buffer(8)       

buffer.value='a\0bc1234'        

print "Original value => %r" % (buffer.raw,)    
print "Interpreted value => %r" % (buffer.value,)

ctypes模块在包含空字符的字符串中会产生截断,上面代码输出如下:

Original value => 'a\x00bc1234' 
Interpreted value => 'a'

这一点和C处理字符串是一样的,会把空字符作为一行的终止。Python在这种情况下使用ctypes,就会继承相同的逻辑,所以字符串就被截断了。解决方案就是对数据进行确认,删除字符串中的空字符来保护字符串或者是禁止使用ctypes

Python Interpreter

#!python
try:    
    if 0:   
        yield 5 
    print("T1-FAIL")    
except Exception as e:  
    print("T1-PASS")    
    pass        

try:    
    if False:   
        yield 5 
    print("T2-FAIL")    
except Exception as e:  
    print(repr(e))  
    pass

以上的测试代码应该返回一个语法错误:SyntaxError: 'yield' outside function。在不同版本的Python上运行结果如下:

这个问题在最新的Python 2.7.x版本中已经解决,而且避免使用像"if 0:","if False:","while 0:","while False:"之类的结构。

0x04 Files —> sys, os, io, pickle, cpickl


pickle

#!python
import pickle   
import io   
badstring = "cos\nsystem\n(S'ls -la /'\ntR."    
badfile = "./pickle.sec"    
with io.open(badfile, 'wb') as w:   
    w.write(badstring)  
obj = pickle.load(open(badfile))    
print "== Object =="    
print repr(obj)

这里构造恶意序列化字符串,以二进制的形式写入文件中,使用pickle.load()函数加载进行反序列化,还原出原始python对象,从而使用os的system()函数来执行命令"ls -la /"。由于pickle这样安全的设计,就可以借此来执行命令了。代码输出结果如下:

  • Linux

    total 104
    drwxr-xr-x  23 root root  4096 Oct 20 11:19 .
    drwxr-xr-x  23 root root  4096 Oct 20 11:19 ..
    drwxr-xr-x   2 root root  4096 Oct  4 00:05 bin
    drwxr-xr-x   4 root root  4096 Oct  4 00:07 boot
    ...
    
  • Mac OS X

    total 16492 
    drwxr-xr-x    31 root wheel     1122 12 Oct 18:58 . 
    drwxr-xr-x    31 root wheel     1122 12 Oct 18:58 ..    
    drwxrwxr-x+  122 root wheel     4148 10 Oct 15:19 Applications
    drwxr-xr-x+   68 root wheel     2312  3 Sep 10:47 Library
    ...
    

pickle / cPickle

#!python
import cPickle  
import traceback    
import sys  
# bignum = int((2**31)-1) # 2147483647 -> OK    
bignum = int(2**31) # 2147483648 -> Max 32bit -> Crash  
random_string = os.urandom(bignum)  
print ("STRING-LENGTH-1=%r") % (len(random_string)) 
fout = open('test.pickle', 'wb')    
try:    
    cPickle.dump(random_string, fout)   
except Exception as e:  
    print "###### ERROR-WRITE ######"   
    print sys.exc_info()[0] 
    raise   
fout.close()    
fin = open('test.pickle', 'rb') 
try:    
    random_string2 = cPickle.load(fin)  
except Exception as e:  
    print "###### ERROR-READ ######"    
    print sys.exc_info()[0] 
    raise   
print ("STRING-LENGTH-2=%r") % (len(random_string2))    
print random_string == random_string2   
sys.exit(0)

在上面的代码中,根据使用的Python版本不同,picklecPickle要么保存截断的数据而没有错误要么就会保存限制为32bit的部分。而且根据Python在操作系统上安装时编译的情况,它会返回在请求随机数据大小上的错误,或者是报告无效参数的OS错误:

  • cPickle (debian 7 x64)

    #!python
    STRING-LENGTH-1=2147483648  
    ###### ERROR-WRITE ######   
    <type 'exceptions.MemoryError'> 
    Traceback (most recent call last):  
    ....    
        pickle.dump(random_string, fout)    
    SystemError: error return without exception set
    
  • pickle (debian 7 x64)

    #!python
    STRING-LENGTH-1=2147483648  
    ###### ERROR-WRITE ######   
    <type 'exceptions.MemoryError'> 
    Traceback (most recent call last):  
    ....    
    File "/usr/lib/python2.7/pickle.py", line 488,
    in save_string
    self.write(STRING + repr(obj)+ '\n')    
    MemoryError
    

解决方案就是执行强大的数据检测来确保不会执行危险行为,还有即使在64位的操作系统上也要限制数据到32位大小。

File Open

#!python
import os   
import sys  
FPATH = 'bug2091.test'  
# ==========================    
print 'wa (1)_write1'   
with open(FPATH, 'wa') as fp:   
    fp.write('test1-')  
with open(FPATH, 'rb') as fp:   
    print repr(fp.read())   
# ==========================    
print 'rU+_write2'  
with open(FPATH, 'rU+') as fp:  
    fp.write('test2-')  
with open(FPATH, 'rb') as fp:   
    print repr(fp.read())   
# ==========================    
print 'wa (2)_write3'   
with open(FPATH, 'wa+') as fp:  
    fp.write('test3-')  
with open(FPATH, 'rb') as fp:   
    print repr(fp.read())   
# ==========================    
print 'aw_write4'   
with open(FPATH, 'aw') as fp:   
    fp.write('test4-')  
with open(FPATH, 'rb') as fp:   
    print repr(fp.read())   
# ==========================    
print 'rU+_read1',  
with open(FPATH, 'rU+') as fp:  
    print repr(fp.read())   
# ==========================    
print 'read_2', 
with open(FPATH, 'read') as fp: 
    print repr(fp.read())   
# ==========================    
os.unlink(FPATH)    
sys.exit(0)

以上代码主要是测试各种文件的打开模式,其中U是指以统一的换行模式打开(不赞成使用),各个平台的测试结果如下:

  • Linux and Mac OS X

  • Windows

INVALID stream operations - Linux / OS X

#!python
import sys  
import io   
fd = io.open(sys.stdout.fileno(), 'wb') 
fd.close()  
try:    
    sys.stdout.write("test for error")  
except Exception:   
    raise

代码在这里使用fileno()来获取sys.stdout的文件描述符,在读写后就关闭,之后便无法从标准输入往标准输出中发送数据流了。输出如下:

  • Python 2.6.5, 2.7.4

    #!python
    close failed in file object destructor: 
    sys.excepthook is missing   
    lost sys.stderr
    
  • Python 2.7.10

    #!python
    Traceback (most recent call last):  
        File "tester.py", line 6, in <module>   
            sys.stdout.write("test for error")  
    IOError: [Errno 9] Bad file descriptor
    

INVALID stream operations - Windows

#!python
import io
import sys    

fd = io.open(sys.stdout.fileno(), 'wb')
fd.close()
sys.stdout.write(“Crash")

在windows上也是类似的,如图:

解决方案就是file和stream库虽然不遵循OS规范,但它们使用一个通用的逻辑,有必要为每个OS使用有处理能力的库,来设置正确的调用过程。

File Write

#!python
import os
import sys  
testfile = 'tempA'  
with open(testfile, "ab") as f: 
    f.write(b"abcd")    
    f.write(b"x" * (1024 ** 2)) 
#########################################   
import io   
testfilea = 'tempB' 
with io.open(testfilea, "ab") as f: 
    f.write(b"abcd")    
    f.write(b"x" * (1024 ** 2))

我们在Linux上使用strace python -OOBRttu script.py来检测Python的写文件行为:

在这里我们想要写入的字符数目是4 + 1048576 = 1048580,在不同的版本上对调用open()和使用io模块进行比较:

  • PYTHON 2.6

    • 调用open()的输出为:

      write(3, "abcdxxxxxxxxxxxxxxxxxxxxxxxxxxxx"..., 4096) = 4096
      write(3, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"..., 1044480) = 1044480
      

      第一次调用的时候被缓冲,不仅仅是写入了4个字符(abcd),还写入了4092个x;第2次调用总共写入1044480个x。这样加起来1044480 + 4096 = 1.048.576,相比1048580就少了4个x。等待5秒就可以解决这个问题,因为操作系统flush了缓存。

    • 调用io模块的输出为:

      write(3, "abcd", 4)                     = 4
      write(3, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"..., 1048576) = 1048576
      

      这样一切就很正常

  • PYTHON 2.7

    • open()的输出为:

      write(3, "abcdxxxxxxxxxxxxxxxxxxxxxxxxxxxx"..., 4096) = 4.096
      write(3, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"..., 1044480) = 1.044.480
      write(3, "xxxx", 4) = 4
      

      在这里进行了三次调用,最后再写入4个x,保证整体数据的正确性。问题就在于这里使用了3次调用而不是我们预期的2次调用。

    • 调用io模块则一切正常

  • PYTHON 3.x

    在Python3中用open()函数和io模块则一切都很正常

在Python2中没有包含原子操作,核心库是在使用缓存进行读写。所以应该尽量去使用io模块。

0x05 Protocols —> socket, poplib, urllib, urllib2


httplib, smtplib, ftplib...

核心库是独立于操作系统的,开发者必须要知道如何为每一个操作系统构建合适的通信通道,而且这些库将会运行执行那些不安全且不正确的操作

#!python
import SimpleHTTPServer 
httplib, smtplib, ftplib...
import SocketServer 
PORT = 45678    
def do_GET(self):   
    self.send_response(200) 
    self.end_headers()  
Handler = SimpleHTTPServer.SimpleHTTPRequestHandler 
Handler.do_GET = do_GET 
httpd = SocketServer.TCPServer(("", PORT), Handler) 
httpd.serve_forever()

在上面的代码中构造了一个HTTP服务端,如果一个客户端连接进来,再去关闭服务端,Python将不会释放资源,操作系统也不会释放socket,引发报错为socket.error: [Errno 48] Address already in use。可以通过以下代码来解决:

#!python
import socket   
import SimpleHTTPServer 
import SocketServer 
PORT = 8080 
# ESSENTIAL: socket resuse is setup BEFORE it is bound. 
# This will avoid TIME_WAIT issues and socket in use errors 
class MyTCPServer(SocketServer.TCPServer):  
    def server_bind(self):  
        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)   
        self.socket.bind(self.server_address)   
def do_GET(self):   
    self.send_response(200) 
    self.end_headers()  
Handler = SimpleHTTPServer.SimpleHTTPRequestHandler 
Handler.do_GET = do_GET 
httpd = MyTCPServer(("", PORT), Handler)    
httpd.serve_forever()

解决方案就是每一个协议库都应该由这样的库封装:为每一个OS和协议都适当地建立和撤销通信,并释放资源

poplib, httplib ...

服务端:

#!python
import socket   
HOST = '127.0.0.1'  
PORT = 45678    
NULLS = '\0' * (1024 * 1024) # 1 MB 
try:    
    sock = socket.socket()  
    sock.bind((HOST, PORT)) 
    sock.listen(1)  
    while 1:    
        print "Waiting connection..."   
        conn, _ = sock.accept() 
        print "Sending welcome..."  
        conn.sendall("+OK THIS IS A TEST\r\n")  
        conn.recv(4096) 
        DATA = NULLS    
        try:    
            while 1:    
                print "Sending 1 GB..." 
                for _ in xrange(1024):  
                    conn.sendall(DATA)  
        except IOError, ex: 
            print "Error: %r" % str(ex) 
        print "End session."    
        print   
finally:    
    sock.close()    
print "End server."

客户端:

#!python
import poplib   
import sys  
HOST = '127.0.0.1'  
PORT = 45678    
try:    
    print "Connecting to %r:%d..." % (HOST, PORT)   
    pop = poplib.POP3(HOST, PORT)   
    print "Welcome:", repr(pop.welcome) 
    print "Listing..."  
    reply = pop.list()  
    print "LIST:", repr(reply)  
except Exception, ex:   
    print "Error: %r" % str(ex) 
print "End."    
sys.exit(0)

以上代码当中,首先开启一个虚拟的服务端,使用客户端去连接服务端,然后服务端开始发送空字符,客户端持续性接收空字符,最后到客户端内存填满,系统崩溃,输出如下:

  • 服务端

    #!python
    Waiting connection...   
    Sending welcome...  
    Sending 1 GB... 
    Error: '[Errno 54] Connection reset by peer'    
    End session.
    
  • 客户端

    • Python >= 2.7.9, 3.3

      #!python
      Connecting to '127.0.0.1':45678...  
      Welcome: '+OK THIS IS A TEST'   
      Listing...  
      Error: 'line too long'  
      End.
      
    • Python < 2.7.9, 3.3

      #!python
      Client!
      Connecting to '127.0.0.1':45678...  
      Welcome: '+OK THIS IS A TEST'   
      ........    
      Error: 'out of memory'
      

解决方案就是如果无法控制检查数据的类型和大小,就使用Python > 2.7.9'或者'Python > 3.3'的版本

对数据没有进行限制的库:

urllib, urllib2

#!python
import io   
import os   
import urllib2 #but all fine with urllib    
domain = 'ftp://ftp.ripe.net'   
location = '/pub/stats/ripencc/'    
file = 'delegated-ripencc-extended-latest'  
url = domain + location + file  
data = urllib2.urlopen(url).read()  
with io.open(file, 'wb') as w:  
    w.write(data)   
file_size = os.stat(file).st_size   
print "Filesize: %s" % (file_size)

urllib2并没有合适的逻辑来处理数据流而且每次都会失败,将上次代码运行三次都会得到错误的文件大小的输出:

Filesize: 65536
Filesize: 32768
Filesize: 49152

如果使用以下的代码则会产生正确的输出:

#!python
import os   
import io   
import urllib2  
domain = 'ftp://ftp.ripe.net'   
location = '/pub/stats/ripencc/'    
file = 'delegated-ripencc-extended-latest'  
with io.open(file, 'wb') as w:  
    url = domain + location + file  
    response = urllib2.urlopen(url) 
    data = response.read()  
    w.write(data)   
file_size = os.stat(file).st_size   
print "Filesize: %s" % (file_size)

输出为:

Filesize: 6598450
Filesize: 6598450
Filesize: 6598450

通过以上的例子可以看出,解决方案为利用操作系统来保证数据流的正确性

已知不安全的库:

最后,当数百万人在使用它的时候,永远不要以为它会一直按你期望的那样运作,也绝对不要以为在使用它的时候是安全的


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK