當(dāng)我們對(duì)不可信的用戶輸入使用str.format的時(shí)候,將會(huì)帶來安全隱患——對(duì)于這個(gè)問題,其實(shí)我早就知道了,但是直到今天我才真正意識(shí)到它的嚴(yán)重性。因?yàn)楣粽呖梢岳盟鼇砝@過Jinja2沙盒,這會(huì)造成嚴(yán)重的信息泄露問題。同時(shí),我在本文最后部分為str.format提供了一個(gè)新的安全版本。
需要提醒的是,這是一個(gè)相當(dāng)嚴(yán)重的安全隱患,這里之所以撰文介紹,是因?yàn)榇蠖鄶?shù)人很可能不知道它是多么容易被利用。
從Python 2.6開始,Python受.NET啟發(fā)而引入了一種格式化字符串的新型語(yǔ)法。當(dāng)然,除了Python之外,Rust及其他一些編程語(yǔ)言也支持這種語(yǔ)法。借助于.format()方法,該語(yǔ)法可以應(yīng)用到字節(jié)和unicode字符串(在Python 3中,只能用于unicode字符串)上面,此外,它還能映射為更加具有可定制性的string.Formatter API。
該語(yǔ)法的一個(gè)特點(diǎn)是,人們可以通過它確定出字符串格式的位置和關(guān)鍵字參數(shù),并且隨時(shí)可以顯式對(duì)數(shù)據(jù)項(xiàng)重新排序。此外,它甚至可以訪問對(duì)象的屬性和數(shù)據(jù)項(xiàng)——這是導(dǎo)致這里的安全問題的根本原因。
總的來說,人們可以利用它來進(jìn)行以下事情:
>>> 'class of {0} is {0.__class__}'.format(42) "class of 42 is "
實(shí)質(zhì)上,任何能夠控制格式字符串的人都有可能訪問對(duì)象的各種內(nèi)部屬性。
第一個(gè)問題是,如何控制格式字符串??梢詮南铝械胤较率郑?/p>
1.字符串文件中不可信的翻譯器。我們很可能通過它們得手,因?yàn)樵S多被翻譯成多種語(yǔ)言的應(yīng)用程序都會(huì)用到這種新式Python字符串格式化方法,但是并非所有人都會(huì)對(duì)輸入的所有字符串進(jìn)行全面的審查。
2.用戶暴露的配置。 由于一些系統(tǒng)用戶可以對(duì)某些行為進(jìn)行配置,而這些配置有可能以格式字符串的形式被暴露出來。需要特別提示的是,我就見過某些用戶可以通過Web應(yīng)用程序來配置通知郵件、日志消息格式或其他基本模板。
如果只是向該格式字符串傳遞C解釋器對(duì)象的話,倒是不會(huì)有太大的危險(xiǎn),因?yàn)檫@樣的話,你最多會(huì)暴露一些整數(shù)類之類的東西。
然而,一旦Python對(duì)象被傳遞給這種格式字符串的話,那就麻煩了。這是因?yàn)椋軌驈腜ython函數(shù)暴露的東西的數(shù)量是相當(dāng)驚人的。 下面是假想的Web應(yīng)用程序的情形,這種情況下能夠泄露密鑰:
CONFIG = { 'SECRET_KEY': 'super secret key' } class Event(object): def __init__(self, id, level, message): self.id = id self.level = level self.message = message def format_event(format_string, event): return format_string.format(event=event)
如果用戶可以在這里注入format_string,那么他們就能發(fā)現(xiàn)下面這樣的秘密字符串:
{event.__init__.__globals__[CONFIG][SECRET_KEY]}
那么,如果需要讓其他人提供格式化字符串,那該怎么辦呢? 其實(shí),可以利用某些未公開的內(nèi)部機(jī)制來改變字符串格式化行為。
from string import Formatter from collections import Mapping class MagicFormatMapping(Mapping): """This class implements a dummy wrapper to fix a bug in the Python standard library for string formatting. See http://bugs.python.org/issue13598 for information about why this is necessary. """ def __init__(self, args, kwargs): self._args = args self._kwargs = kwargs self._last_index = 0 def __getitem__(self, key): if key == '': idx = self._last_index self._last_index += 1 try: return self._args[idx] except LookupError: pass key = str(idx) return self._kwargs[key] def __iter__(self): return iter(self._kwargs) def __len__(self): return len(self._kwargs) # This is a necessary API but it's undocumented and moved around # between Python releases try: from _string import formatter_field_name_split except ImportError: formatter_field_name_split = lambda x: x._formatter_field_name_split() {C} class SafeFormatter(Formatter): def get_field(self, field_name, args, kwargs): first, rest = formatter_field_name_split(field_name) obj = self.get_value(first, args, kwargs) for is_attr, i in rest: if is_attr: obj = safe_getattr(obj, i) else: obj = obj[i] return obj, first def safe_getattr(obj, attr): # Expand the logic here. For instance on 2.x you will also need # to disallow func_globals, on 3.x you will also need to hide # things like cr_frame and others. So ideally have a list of # objects that are entirely unsafe to access. if attr[:1] == '_': raise AttributeError(attr) return getattr(obj, attr) def safe_format(_string, *args, **kwargs): formatter = SafeFormatter() kwargs = MagicFormatMapping(args, kwargs) return formatter.vformat(_string, args, kwargs)
現(xiàn)在,我們就可以使用safe_format方法來替代str.format了:
>>> '{0.__class__}'.format(42) "" >>> safe_format('{0.__class__}', 42) Traceback (most recent call last): File "", line 1, in AttributeError: __class__
程序開發(fā)中有這么一句話:任何時(shí)候不要相信用戶的輸入!現(xiàn)在看來這句話說得非常有道理。所以各位同學(xué)要謹(jǐn)記!
【課程推薦】
Python免費(fèi)在線視頻教程
聲明:本網(wǎng)頁(yè)內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com