Python安全編碼指南

jopen 9年前發布 | 23K 次閱讀 Python Python開發

0x00 前言


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

這個pdf中深入Python的核心庫進行分析,并且探討了在兩年的安全代碼審查過程中,一些被認為是最關鍵的問題,最后也提出了一些解決方案和緩解的方法。我自己也在驗證探究過程中添油加醋了一點,如有錯誤還請指出哈。

下面一張圖表示他們的方法論:

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 可調用的數據類型有:

Python安全編碼指南

問題在于:

  • 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以二進制的形式寫入文件,再分別用codecs和io模塊進行讀取,編碼形式為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安全編碼指南

這個問題在最新的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版本不同,pickle或cPickle要么保存截斷的數據而沒有錯誤要么就會保存限制為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

    Python安全編碼指南

  • Windows

    Python安全編碼指南

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上也是類似的,如圖:

Python安全編碼指南

解決方案就是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'的版本

對數據沒有進行限制的庫:

Python安全編碼指南

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

通過以上的例子可以看出,解決方案為利用操作系統來保證數據流的正確性

已知不安全的庫:

Python安全編碼指南

最后,當數百萬人在使用它的時候,永遠不要以為它會一直按你期望的那樣運作,也絕對不要以為在使用它的時候是安全的

來自:http://drops.wooyun.org/tips/10383

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!