最近一個python字串格式化的漏洞引起了我的注意,今天就來說一下Python引入的一種格式化字串的新型語法的安全漏洞進行了深入的分析,並提供了相應的安全解決方案。
當我們對不可信的使用者輸入使用str.format的時候,將會帶來安全隱患——對於這個問題,其實我早就知道了,但是直到今天我才真正意識到它的嚴重性。因為攻擊者可以利用它來繞過Jinja2沙盒,這會造成嚴重的資訊洩露問題。同時,我在本文最後部分為str.format提供了一個新的安全版本。
需要提醒的是,這是一個相當嚴重的安全隱患,這裡之所以撰文介紹,是因為大多數人很可能不知道它是多麼容易被利用。
核心問題
從Python 2.6開始,Python受.NET啟發而引入了一種格式化字串的新型語法。當然,除了Python之外,Rust及其他一些程式語言也支援這種語法。藉助於.format()方法,該語法可以應用到位元組和unicode字串(在Python 3中,只能用於unicode字串)上面,此外,它還能對映為更加具有可定製性的string.Formatter API。
該語法的一個特點是,人們可以透過它確定出字串格式的位置和關鍵字引數,並且隨時可以顯式對資料項重新排序。此外,它甚至可以訪問物件的屬性和資料項——這是導致這裡的安全問題的根本原因。
總的來說,人們可以利用它來進行以下事情:
>>> 'class of {0} is {0.__class__}'.format(42) "class of 42 is "
實質上,任何能夠控制格式字串的人都有可能訪問物件的各種內部屬性。
問題出在哪裡?
第一個問題是,如何控制格式字串。可以從下列地方下手:
1.字串檔案中不可信的翻譯器。我們很可能透過它們得手,因為許多被翻譯成多種語言的應用程式都會用到這種新式Python字串格式化方法,但是並非所有人都會對輸入的所有字串進行全面的審查。
2.使用者暴露的配置。 由於一些系統使用者可以對某些行為進行配置,而這些配置有可能以格式字串的形式被暴露出來。需要特別提示的是,我就見過某些使用者可以透過Web應用程式來配置通知郵件、日誌訊息格式或其他基本模板。
危險等級
如果只是向該格式字串傳遞C直譯器物件的話,倒是不會有太大的危險,因為這樣的話,你最多會暴露一些整數類之類的東西。
然而,一旦Python物件被傳遞給這種格式字串的話,那就麻煩了。這是因為,能夠從Python函式暴露的東西的數量是相當驚人的。 下面是假想的Web應用程式的情形,這種情況下能夠洩露金鑰:
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,那麼他們就能發現下面這樣的秘密字串:
{event.__init__.__globals__[CONFIG][SECRET_KEY]}
將格式化作沙箱化處理
那麼,如果需要讓其他人提供格式化字串,那該怎麼辦呢? 其實,可以利用某些未公開的內部機制來改變字串格式化行為。
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)
現在,我們就可以使用safe_format方法來替代str.format了:
>>> '{0.__class__}'.format(42) "" >>> safe_format('{0.__class__}', 42) Traceback (most recent call last): File "", line 1, in AttributeError: __class__
總結:
程式開發中有這麼一句話:任何時候不要相信使用者的輸入!現在看來這句話說得非常有道理。所以各位同學要謹記!