軟體複用導致的軟體依賴問題 - research!rsc

banq發表於2019-01-24

幾十年來,對軟體重用的討論遠比實際的軟體重用更常見。今天,情況正好相反:開發人員每天都以軟體依賴的形式重複使用其他人編寫的軟體,而且情況大多未經審查。
我自己的背景包括使用Google的內部原始碼系統十年,該系統將軟體依賴關係視為一流的概念,1 並且還開發了對Go程式語言中依賴關係的支援。2
軟體依賴性帶來了經常被忽視的嚴重風險。轉向簡單,細粒度的軟體重用發生得如此之快,以至於我們還沒有理解有效選擇和使用依賴關係的最佳實踐,甚至無法確定何時適當和何時適用。我寫這篇文章的目的是提高對風險的認識,並鼓勵對解決方案進行更多研究。

什麼是依賴?
在今天的軟體開發世界中,依賴項是您希望從程式中呼叫的附加程式碼。新增依賴項可避免重複已完成的工作:設計,編寫,測試,除錯和維護特定的程式碼單元。在本文中,我們將該程式碼單元稱為 ; 一些系統使用諸如庫或模組之類的術語而不是包。
承擔外部編寫的依賴是一個古老的做法:大多數程式設計師在他們的職業生涯中必須經歷手動下載和安裝所需庫的步驟,如C的PCRE或zlib,或C ++的Boost或Qt,或Java的JodaTime或JUnit。這些軟體包包含高質量的除錯程式碼,需要大量的專業知識才能開發。對於需要其中一個軟體包提供的功能的程式,手動下載,安裝和更新軟體包的繁瑣工作比從頭開始重新開發該功能的工作更容易。但是,重用本身的高成本意味著重用依賴的第三方軟體包往往很大:通常只需要一個小庫包將更容易重新實現。
一個依賴管理器 (有時稱為包管理器)可以自動依賴包的下載和安裝。由於依賴關係管理器使單個程式包更易於下載和安裝,因此較低的固定成本使得較小的程式包可以經濟地釋出和重用。
例如,Node.js依賴關係管理器NPM提供對超過750,000個包的訪問。其中之一escape-string-regexp,提供了一個在其輸入中轉義正規表示式運算子的函式。整個實施是:

var matchOperatorsRe = /[|\\ {}()[\] ^ $ + *。?] / g; 

module.exports = function(str){ 
    if(typeof str!=='string'){ 
        throw new TypeError('Expected a string'); 
    } 
    return str.replace(matchOperatorsRe,'\\ $&'); 
};


在依賴管理器之前,釋出一個八行程式碼庫是不可想象的:太多的開銷,太少的好處。但NPM已將開銷大約推至零,結果是我們可以打包和重用幾乎無關緊要的一些功能。在2019年1月下旬,該escape-string-regexp軟體包明確依賴於近千個其他NPM軟體包,更不用說開發人員為自己使用而不共享的所有軟體包。

現在依賴於每種程式語言的依賴管理器。Maven Central(Java),Nuget(.NET),Packagist(PHP),PyPI(Python)和RubyGems(Ruby)每個託管超過100,000個包。這種細粒度,廣泛的軟體重用的到來是過去二十年中軟體開發中最重要的變化之一。如果我們不小心,它將導致嚴重的問題。

怎麼可能出錯?
包是從Internet下載的程式碼,新增一個包作為依賴包將開發程式碼設計,編寫,測試,除錯和維護的工作外包給網際網路上的其他人,這是你經常不認識的人。透過使用該程式碼,您將自己的程式暴露給依賴項中的所有失敗和缺陷。你的程式執行現在字面上取決於 從這個陌生人在網際網路上下載的程式碼。以這種方式呈現,聽起來非常不安全。為什麼有人會這樣做?
我們之所以這樣做是因為它很容易,因為它似乎有效,因為其他人也都這樣做,而且最重要的是,因為它似乎是古老慣例的自然延續。但是我們忽略了一些重要的差異。
幾十年前,大多數開發人員已經信任其他人編寫他們所依賴的軟體,例如作業系統和編譯器。該軟體是從已知來源購買的,通常帶有某種支援協議。仍然存在潛在的漏洞或徹頭徹尾的惡作劇3 ,但至少我們知道我們正在和誰打交道,並且通常有商業或法律資源。
透過網際網路免費分發的開源軟體現象取代了許多早期的軟體購買方式,這些專案建立了眾所周知的聲譽,這些聲譽往往是人們決定使用因素中的重要因素,信任我們軟體資源的商業和法律支援被聲譽支援所取代。許多常見的早期包庫仍然享有良好的聲譽:考慮BLAS(1979年出版),Netlib(1987),libjpeg(1991),LAPACK(1992),HP STL(1994)和zlib(1995)。
依賴管理器已經縮減了這個開原始碼重用模型:現在,開發人員可以按照幾十行的各個函式的粒度共享程式碼。這是一項重大的技術成就。有無數可用的軟體包,編寫程式碼可能涉及如此大量的軟體包,但信任程式碼的商業,法律和聲譽支援機制尚未延續。我們相信更多的程式碼而沒有理由這麼做。
採用不良依賴的成本可以被視為所有可能的不良結果的總和,即每個不良結果的成本乘以其發生的機率(風險)。

軟體複用導致的軟體依賴問題 - research!rsc
使用依賴關係的上下文決定了不良結果的成本。一方面是一個個人愛好專案,其中大多數不良後果的成本幾乎為零:你只是玩得開心,除了浪費一些時間之外,bug沒有真正的影響,甚至除錯它們也很有趣。所以風險機率幾乎無關緊要:它被乘以零;另一方面,生產軟體必須保持多年。在這裡,依賴關係中的錯誤成本可能非常高:伺服器可能會關閉,敏感資料可能會被洩露,客戶可能會受到損害,公司可能會失敗。高故障成本使得估計並降低嚴重故障風險變得更加重要。
無論預期成本如何,具有較大依賴性的經驗都表明了一些估計和降低新增軟體依賴性風險的方法。可能需要更好的工具來幫助降低這些方法的成本,就像依賴管理者迄今為止一直專注於降低下載和安裝成本一樣。

檢查依賴性
你不會僱用一個你從未聽說過的軟體開發人員。您將首先了解有關應聘者的更多資訊:檢查,進行面試,執行背景檢查等。在您依賴於在網際網路上找到的包裹之前,首先同樣謹慎的要了解一下它。

基本檢查可以讓您瞭解嘗試使用此程式碼時遇到問題的可能性。如果檢查發現可能存在小問題,您可以採取措施準備或避免它們。如果檢查發現主要問題,最好不要使用這個包:也許你會找到一個更合適的,或者你需要自己開發一個。請記住,開源軟體包是由作者釋出的,希望它們有用,但不保證可用性或支援。在生產中斷的過程中,您將成為除錯它的人。正如最初的GNU通用公共許可證所警告的那樣,“程式的質量和效能的全部風險都與您同在。如果程式證明有缺陷,您將承擔所有必要的維修,修理或更正的費用。“ 


本節的其餘部分概述了檢查包裝並決定是否依賴它時的一些注意事項。

設計
包檔案是否清晰?API是否有清晰的設計?如果作者能夠很好地解釋軟體包的API及其設計,那麼使用者在文件中會增加他們在原始碼中很好地向計算機解釋實現的可能性。為清晰,設計良好的API編寫程式碼也更容易,更快速,並且希望減少錯誤。作者是否記錄了他們對客戶端程式碼的期望,以便將來的升級相容?(示例包括C ++ 5和Go 6相容性文件。)

程式碼質量
程式碼編寫得很好嗎?閱讀其中一些內容。作者是否認真,盡職盡責,一致?它看起來像你想要除錯的程式碼嗎?
開發自己的系統方法來檢查程式碼質量。例如,像編譯C或C ++程式一樣簡單,啟用了重要的編譯器警告(例如-Wall),可以讓您瞭解開發人員如何認真地避免各種未定義的行為。Go,Rust和Swift等最新語言使用unsafe關鍵字來標記違反型別系統的程式碼; 看看有多少不安全的程式碼。更高階的語義工具如Infer 7或SpotBugs 8也很有用。Linters的幫助不大:你應該忽略關於大括號樣式等主題的死記硬背建議,而不是關注語義問題。

對您可能不熟悉的開發實踐持開放態度。例如,SQLite庫作為單個200,000行C原始檔和單個11,000行標題釋出,即“合併”。這些檔案的大小應該引發一個初始的紅旗,但更密切的調查會發現實際開發原始碼,一個包含一百多個C原始檔,測試和支援指令碼的傳統檔案樹。事實證明,單檔案分發是從原始源自動構建的,對終端使用者來說更容易,特別是那些沒有依賴管理器的使用者。(編譯後的程式碼也執行得更快,因為編譯器可以看到更多的最佳化機會。)

測試
程式碼是否有測試?你能跑嗎?他們透過了嗎?測試確定程式碼的基本功能是正確的,並且它們表明開發人員認真對待它是否正確。例如,SQLite開發樹有一個非常全面的測試套件,包含超過30,000個單獨的測試用例以及解釋測試策略的開發人員文件。9 另一方面,如果測試很少或沒有測試,或者測試失敗,這是一個嚴重的危險訊號:未來對軟體包的更改可能會引入很容易被捕獲的迴歸。如果你堅持用自己編寫的程式碼進行測試(你這樣做,對嗎?),你應該堅持用你外包給別人的程式碼進行測試。
假設存在的測試,執行和透過,你可以透過像程式碼覆蓋分析,競爭檢測,執行時間儀表執行它們收集更多的資訊,10 記憶體分配檢查和記憶體洩漏檢測。

除錯
找到包的問題跟蹤器。有很多開放的bug報告嗎?他們開了多久了?有很多修復錯誤嗎?最近有沒有修復過的bug?如果你看到許多關於什麼看起來像真正的錯誤的公開問題,特別是如果它們已經開啟了很長時間,那不是一個好兆頭。另一方面,如果很少發現錯誤並立即修復,那就太好了。

維護
檢視包的提交歷史記錄。程式碼被維護了多長時間?現在是否積極維護?已經積極維護了較長時間的軟體包更有可能繼續維護。有多少人在包裝上工作?許多軟體包是開發人員在業餘時間建立和共享的個人專案。其他人是一群付費開發人員數千小時工作的結果。一般來說,後一種軟體包更有可能及時修復錯誤,穩定改進和一般維護。
另一方面,一些程式碼確實是“完成的”。例如,escape-string-regexp前面顯示的NPM 可能永遠不需要再次修改。

用法
許多其他軟體包是否依賴於此程式碼?依賴關係管理器通常可以提供有關使用情況的統計資訊,或者您可以使用Web搜尋來估計其他人編寫有關使用該軟體包的頻率。更多使用者應該至少意味著程式碼執行良好的更多人,以及更快地檢測新錯誤。廣泛使用也是對持續維護問題的對沖:如果廣泛使用的軟體包丟失其維護者,感興趣的使用者可能會向前邁進。
例如,像PCRE或Boost或JUnit這樣的庫被廣泛使用。這使得它更有可能 - 雖然肯定不能保證 - 你可能遇到的錯誤已經被修復,因為其他人首先遇到它們。

安全
您是否會使用包處理不受信任的輸入?如果是這樣,它是否能夠抵禦惡意輸入?是否有國家漏洞資料庫(NVD)中列出的安全問題歷史記錄?

許可
程式碼是否獲得了適當許可?它有許可證嗎?許可證是否適用於您的專案或公司?GitHub上的一小部分專案沒有明確的許可證。您的專案或公司可能會對允許的依賴許可證施加進一步的限制。例如,Google不允許使用根據AGPL類許可證(過於繁瑣)獲得許可的程式碼以及類似WTFPL的許可證(過於模糊)

依賴
程式碼是否具有自己的依賴關係?間接依賴中的缺陷對於您的程式來說就像直接依賴中的缺陷一樣糟糕。依賴關係管理器可以列出給定包的所有傳遞依賴關係,並且理想情況下應按照本節中的描述檢查它們中的每一個。具有許多依賴關係的包會產生額外的檢查工作,因為這些相同的依賴關係會產生需要評估的額外風險。
許多開發人員從未檢視過程式碼的傳遞依賴關係的完整列表,也不知道他們依賴的是什麼。例如,在2016年3月,NPM使用者社群發現許多流行專案 - 包括Babel,Ember和React-all都間接依賴於一個名為的小包left-pad,由一個8行功能體組成。當left-pad從NPM 中刪除該包的作者無意中打破了大多數Node.js使用者的構建時,他們發現了這一點。14 而left-pad在這方面幾乎沒有例外。例如,NPM上釋出的750,000個軟體包中有30%至少是間接依賴的escape-string-regexp。根據Leslie Lamport對分散式系統的觀察,依賴管理器可以輕鬆地建立一種情況,在該情況下,您甚至不知道存在的包的失敗會導致您自己的程式碼無法使用。

測試依賴性
檢查過程應包括執行包自己的測試。如果程式包透過檢查並且您決定使專案依賴於它,則下一步應該是編寫針對應用程式所需功能的新測試。這些測試通常以編寫簡短的獨立程式開始,以確保您能夠理解程式包的API,並且它可以執行您認為的功能。(如果您不能或不能,請立即返回!)然後需要額外的努力將這些程式轉換為可以針對較新版本的程式包執行的自動化測試。如果您發現了一個錯誤並且有可能的修復,那麼您將希望能夠輕鬆地重新執行這些專案特定的測試,以確保該修復不會破壞其他任何內容。

抽象依賴
根據包的不同,您可能會在以後重新考慮。也許更新將使包朝著新的方向發展。也許會發現嚴重的安全問題。也許會有更好的選擇。出於所有這些原因,值得努力使您可以輕鬆地將專案遷移到新的依賴項。
如果將從專案原始碼中的許多位置使用該包,則遷移到新的依賴項將需要更改所有這些不同的源位置。更糟糕的是,如果程式包將在您自己的專案的API中公開,那麼遷移到新的依賴項將需要在呼叫API的所有程式碼中進行更改,而這些程式碼可能無法控制。為了避免這些成本,定義您自己的介面以及使用依賴項實現該介面的瘦包裝器是有意義的。請注意,包裝器應僅包含專案所需的依賴項,而不是依賴項提供的所有內容。理想情況下,這允許您稍後透過僅更改包裝器來替換不同的,同樣適當的依賴項。

隔離依賴關係
在執行時隔離依賴項以限制其中的錯誤可能造成的損害也可能是適當的。防禦之一是更好地限制依賴關係可以訪問的內容。

避免依賴
如果一個依賴似乎風險太大而你找不到隔離它的方法,最好的答案可能是完全避免它,或者至少避免你認為最有問題的部分。
如果您只需要一小部分依賴關係,那麼複製您需要的內容可能是最簡單的(當然,保留適當的版權和其他法律宣告)。您負責修復錯誤,維護等等,但您也完全與較大的風險隔離開來。Go開發者社群有一句諺語:“一點點複製比一點依賴更好。

升級依賴項
長期以來,關於軟體的傳統觀點是“如果沒有破壞,就不要修復它。”升級帶來了引入新漏洞的機會;  沒有相應的獎勵 - 就像你需要的新功能 - 為什麼要承擔風險?
該分析忽略了兩個成本。首先是最終升級的成本。在軟體中,進行程式碼更改的難度並不是線性擴充套件的:進行十次小改動比做一次等效的大改變更少工作,更容易糾正。第二個是以艱難的方式發現已經修復的錯誤的成本。特別是在安全環境中,已知的漏洞被積極利用,每天等待的是攻擊者闖入。
及時升級很重要,但升級意味著向專案新增新程式碼,這應該意味著更新您對基於新版本使用依賴項的風險的評估。至少,您需要瀏覽顯示從當前版本到升級版本所做更改的差異,或者至少閱讀發行說明,以確定升級程式碼中最可能關注的區域。如果許多程式碼正在發生變化,那麼難以消化差異,這也是您可以納入風險評估更新的資訊。
您還需要重新執行您編寫的特定於專案的測試,以確保升級的軟體包至少與早期版本一樣適合專案。重新執行包自己的測試也是有意義的。如果軟體包有自己的依賴項,那麼專案的配置完全有可能使用這些依賴項的不同版本(舊版或新版),而不是軟體包的作者使用的版本。執行軟體包自己的測試可以快速識別特定於您的配置的問題。
同樣,升級不應該是完全自動的。在部署升級版本之前,您需要驗證升級版本是否適合您的環境。
如果您的升級過程包括重新執行您已經為依賴項編寫的整合和資格測試,那麼您可能會在新問題到達生產之前識別它們,那麼,在大多數情況下,延遲升級比快速升級更具風險。

觀察您的依賴項
即使在所有這些工作之後,你還沒有完成你的依賴。重要的是繼續監控它們,甚至可能重新評估您使用它們的決定。
首先,確保您繼續使用您認為的特定包版本。現在,大多數依賴關係管理器可以輕鬆甚至自動地記錄給定包版本的預期原始碼的加密雜湊值,然後在另一臺計算機或測試環境中重新下載包時檢查該雜湊值。這可確保您的構建使用您檢查和測試的相同依賴原始碼。這些型別的檢查阻止了event-stream前面描述的攻擊者在已釋出的版本3.3.5中靜默插入惡意程式碼。相反,攻擊者必須建立一個新版本3.3.6,並等待人們升級(不仔細檢視更改)。
同樣重要的是要注意新的間接依賴關係:升級可以輕鬆引入新的軟體包,現在專案的成功依賴於這些軟體包。他們也值得你的關注。在這種情況下event-stream,惡意程式碼隱藏在另一個包中,flatmap-stream新event-stream版本將其新增為新的依賴項。
升級是重新審視使​​用正在發生變化的依賴關係的決定的自然時間。同樣重要的是要定期訪問任何依賴關係發生變化。沒有安全問題或其他錯誤需要修復似乎是否合理?該專案是否已被放棄?也許是時候開始計劃取代這種依賴。
重新檢查每個依賴項的安全歷史記錄也很重要。例如,Apache Struts在2016年,2017年和2018年披露了不同的主要遠端程式碼執行漏洞。即使您擁有執行它並立即更新它們的所有伺服器的列表,該跟蹤記錄可能會讓您重新考慮使用它。


結論
軟體重用終於來了,我並不是要低估它的好處:它為軟體開發人員帶來了巨大的積極轉變。即便如此,我們還是在沒有完全考慮潛在後果的情況下接受了這種轉變。在我們擁有比以往更多的依賴關係的同時,信任依賴關係的舊理由變得越來越無效。
我在本文中概述的對特定依賴項的批判性檢查是一項重要的工作,並且仍然是例外而不是規則。但是我懷疑是否有任何開發人員真正為每一個可能的新依賴項做出這樣的努力。我只為我自己的依賴項的子集做了一個子集。大多數時候,整個決定都是“讓我們看看會發生什麼。”很多時候,除此之外的任何事情似乎都是太多的努力。
但是Copay和Equifax攻擊是我們今天消耗軟體依賴的方式的明顯警告。我們不應該忽視這些警告。我提出了三條廣泛的建議。

  1. 認識到這個問題。 我希望這篇文章讓你相信這裡有一個值得解決的問題。我們需要很多人集中精力解決它。
  2. 建立今天的最佳實踐。 我們需要使用現有的可用內容來建立管理依賴關係的最佳實踐。這意味著制定評估,減少和跟蹤風險的流程,從最初的採用決策到生產使用。事實上,正如一些工程師專注於測試一樣,我們可能需要專門管理依賴關係的工程師。
  3. 為明天開發更好的依賴技術。 依賴管理器基本上消除了下載和安裝依賴項的成本。未來的開發工作應側重於降低使用依賴項所需的評估和維護成本。例如,包發現站點可能會找到更多方法來允許開發人員共享他們的發現。構建工具至少應該能夠輕鬆執行包自己的測試。更積極的是,構建工具和包管理系統也可以一起工作,以允許包作者針對其API的所有公共客戶端測試新的更改。語言還應提供簡單的方法來隔離可疑包。

那裡有很多好的軟體。讓我們一起來了解如何安全地重複使用它。

相關文章