Linux中國

YAML:可能並不是那麼完美

我之前寫過為什麼將 JSON 用於人類可編輯的配置文件是一個壞主意,今天我們將討論 YAML 格式的一些常見問題。

默認情況下不安全

YAML 默認是不安全的。載入用戶提供的(不可信的)YAML 字元串需要仔細考慮。

!!python/object/apply:os.system
args: ['ls /']

print(yaml.load(open('a.yaml'))) 運行它,應該給你這樣的東西:

bin   etc   lib    lost+found  opt   root  sbin  tmp  var sys
boot  dev   efi    home        lib64 mnt   proc  run  srv usr
0

許多其他語言(包括 Ruby 和 PHP 1 )默認情況下也不安全(LCTT 譯註:這裡應該說的是解析 yaml)。在 GitHub 上搜索 yaml.load 會得到驚人的 280 萬個結果,而 yaml.safe_load 只能得到 26000 個結果。

提個醒,很多這樣的 yaml.load() 都工作的很好,在配置文件中載入 yaml.load() 通常沒問題,因為它通常(雖然並不總是!)來自「可靠源」,而且很多都來自靜態的 YAML 測試文件。但是,人們還是不禁懷疑在這 280 萬個結果中隱藏了多少漏洞。

這不是一個理論問題。在 2013 年,正是由於這個問題,所有的 Ruby on Rails 應用程序都被發現易受遠程代碼執行攻擊。

有人可能會反駁說這不是 YAML 格式的錯誤,而是那些庫實現錯誤的的問題,但似乎大多數庫默認不是安全的(特別是動態語言),所以事實上這是 YAML 的一個問題。

有些人可能會反駁認為修復它就像用 safe_load() 替換 load() 一樣容易,但是很多人都沒有意識到這個問題,即使你知道它,它也是很容易忘記的事情之一。這是非常糟糕的 API 設計。

可能很難編輯,特別是對於大文件

YAML 文件可能很難編輯,隨著文件變大,這個難度會快速增大。

一個很好的例子是 Ruby on Rails 的本地化翻譯文件。例如:

en:
   formtastic:
     labels:
       title: "Title"  # Default global value
       article:
         body: "Article content"
       post:
         new:
           title: "Choose a title..."
           body: "Write something..."
         edit:
           title: "Edit title"
           body: "Edit body"

看起來不錯,對吧?但是如果這個文件有 100 行怎麼辦?或者 1,000 行?在文件中很難看到 「where」,因為它可能在屏幕外。你需要向上滾動,但是你需要跟蹤縮進,即使遵循縮進指南也很難,特別是因為 2 個空格縮進是常態而且 製表符縮進被禁止 2

不小心縮進出錯通常不算錯誤,它通常只是反序列化為你不想要的東西。這樣只能祝你調試快樂!

我已經愉快地編寫 Python 長達十多年,所以我已經習慣了顯眼的空白,但有時候我仍在和 YAML 抗爭。在 Python 中,雖然沒有那種長達幾頁的函數,但數據或配置文件的長度沒有這種自然限制,這就帶來了缺點和損失了清晰度。

對於小文件,這不是問題,但它確實無法很好地擴展到較大的文件,特別是如果你以後想編輯它們的話。

這非常複雜

在瀏覽一個基本的例子時,YAML 看似「簡單」和「顯而易見」,但事實證明並非如此。YAML 規範有 23449 個單詞,為了比較,TOML 有 3339 個單詞,Json 有 1969 個單詞,XML 有 20603 個單詞。

我們中有誰讀過全部規範嗎?有誰讀過並理解了全部?誰閱讀過,理解進而記住所有這些?

例如,你知道在 YAML 中編寫多行字元串有 9 種方法嗎?並且它們具有細微的不同行為。

是的 :-/

如果你看一下那篇文章的修訂歷史,它就會變得更加有趣,因為文章的作者發現了越來越多的方法可以實現這一點,以及更多的細微之處。

它從預覽開始告訴我們 YAML 規範,它表明(強調我的):

本節簡要介紹了 YAML 的表達能力。預計初次閱讀的人不可能理解所有的例子。相反,這些選擇用作該規範其餘部分的動機。

令人驚訝的行為

以下會解析成什麼(Colm O』Connor 提供的例子):

- Don Corleone: Do you have faith in my judgment?
- Clemenza: Yes
- Don Corleone: Do I have your loyalty?

結果為:

[
    {'Don Corleone': 'Do you have faith in my judgment?'},
    {'Clemenza': True},
    {'Don Corleone': 'Do I have your loyalty?'}
]

那麼這個呢:

python: 3.5.3
postgres: 9.3

3.5.3 被識別為字元串,但 9.3 被識別為數字而不是字元串:

{'python': '3.5.3', 'postgres': 9.3}

這個呢:

Effenaar: Eindhoven
013: Tilburg

013 蒂爾堡 Tilburg 的一個流行音樂場地,但 YAML 會告訴你錯誤答案,因為它被解析為八進位數字:

{11: 'Tilburg', 'Effenaar': 'Eindhoven'}

所有這一切,以及更多,就是為什麼許多經驗豐富的 YAMLer 經常會將所有字元串用引號引起來的原因,即使它不是嚴格要求。許多人不使用引號,而且很容易忘記,特別是如果文件的其餘部分(可能由其他人編寫)不使用引號。

它不方便

因為它太複雜了,它所聲稱的可移植性被誇大了。例如,考慮以下這個從 YAML 規範中獲取的示例:

? - Detroit Tigers
  - Chicago cubs
:
  - 2001-07-23

? [ New York Yankees,
    Atlanta Braves ]
: [ 2001-07-02, 2001-08-12,
    2001-08-14 ]

拋開大多數讀者可能甚至不知道這是在做什麼之外,請嘗試使用 PyYAML 在 Python 中解析它:

yaml.constructor.ConstructorError: while constructing a mapping
  in "a.yaml", line 1, column 1
found unhashable key
  in "a.yaml", line 1, column 3

在 Ruby 中,它可以工作:

{
    ["Detroit Tigers", "Chicago cubs"] => [
        #<Date: 2001-07-23 ((2452114j,0s,0n),+0s,2299161j)>
    ],
    ["New York Yankees", "Atlanta Braves"] => [
        #<Date: 2001-07-02 ((2452093j,0s,0n),+0s,2299161j)>,
        #<Date: 2001-08-12 ((2452134j,0s,0n),+0s,2299161j)>,
        #<Date: 2001-08-14 ((2452136j,0s,0n),+0s,2299161j)>
    ]
}

這個原因是你不能在 Python 中使用列表作為一個字典的鍵:

>>> {[&apos;a&apos;]: &apos;zxc&apos;}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  TypeError: unhashable type: &apos;list&apos;

而這種限制並不是 Python 特有的,PHP、JavaScript 和 Go 等常用語言都有此限制。

因此,在 YAML 文件中使用這種語法,你將無法在大多數語言中解析它。

這是另一個從 YAML 規範的示例部分中獲取的:

# Ranking of 1998 home runs
- Mark McGwire
- Sammy Sosa
- Ken Griffey

# Team ranking
- Chicago Cubs
- St Louis Cardinals

Python 會輸出:

yaml.composer.ComposerError: expected a single document in the stream
  in "a.yaml", line 3, column 1
but found another document
  in "a.yaml", line 8, column 1

然而 Ruby 輸出:

["Mark McGwire", "Sammy Sosa", "Ken Griffey"]

原因是單個文件中有多個 YAML 文檔(--- 意味開始一個新文檔)。在 Python 中,有一個 load_all 函數來解析所有文檔,而 Ruby 的 load() 只是載入第一個文檔,據我所知,它沒有辦法載入多個文檔。

在實現之間存在很多不兼容

目標實現了嗎?

規範說明:

YAML 的設計目標安裝優先順序降序排列如下:

  1. YAML 很容易被人類閱讀。
  2. YAML 數據在編程語言之間是可移植的。
  3. YAML 匹配敏捷語言的原生數據結構。
  4. YAML 有一個一致的模型來支持通用工具。
  5. YAML 支持一次性處理。
  6. YAML 具有表現力和可擴展性。
  7. YAML 易於實現和使用。

那麼它做的如何呢?

YAML 很容易被人類閱讀。

只有堅持一小部分子集時才有效。完整的規則集很複雜 —— 遠遠超過 XML 或 JSON。

YAML 數據在編程語言之間是可移植的。

事實並非如此,因為創建常見語言不支持的結構太容易了。

YAML 匹配敏捷語言的原生數據結構。

參見上面。另外,為什麼只支持敏捷(或動態)語言?其他語言呢?

YAML 有一個一致的模型來支持通用工具。

我甚至不確定這意味著什麼,我找不到任何詳細說明。

YAML 支持一次性處理。

這點我接受。

YAML 具有表現力和可擴展性。

嗯,是的,但它太富有表現力(例如太複雜)。

YAML 易於實現和使用。

$ cat `ls -1 ~/gocode/src/github.com/go-yaml/yaml/*.go | grep -v _test` | wc -l
9247

$ cat /usr/lib/python3.5/site-packages/yaml/*.py | wc -l
5713

結論

不要誤解我的意思,並不是說 YAML 很糟糕 —— 它肯定不像使用 JSON 那麼多的問題 —— 但它也不是非常好。有一些一開始並不明顯的缺點和驚喜,還有許多更好的替代品,如 TOML 和其他更專業的格式。

就個人而言,當我有選擇時,我不太可能再次使用它。

如果你必須使用 YAML,那麼我建議你使用 StrictYAML,它會刪除一些(雖然不是全部)比較麻煩的部分。

反饋

你可以發送電子郵件至 martin@arp242.net創建 GitHub issue 以獲取反饋、問題等。

腳註

  1. 在 PHP 中你需要修改一個 INI 設置來獲得安全的行為,不能只是調用像 yaml_safe() 這樣的東西。PHP 想盡辦法讓愚蠢的東西越發愚蠢。幹得漂亮!
  2. 不要在這裡做空格與製表符之爭,如果這裡可以用製表符的話,我可以(臨時)增加製表符寬度來使它更易讀——這是製表符的一種用途。

via: https://arp242.net/weblog/yaml_probably_not_so_great_after_all.html

作者:Martin Tournoij 選題:lujun9972 譯者:MjSeven 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國