一些不太容易注意到的 Python 安全性問題

前幾天看到「10 Unknown Security Pitfalls for Python」這篇,講 Python ecosystem 裡面的一些設計造成的安全性問題,裡面很多東西都很有趣,而且有些算是共通的,其他程式語言也會中獎...

第一個是開發者用 assert() 確認權限,但 assert() 的設計是 debug 用的功能,所以可以預期在 optimization 後會被拿掉 (其他程式語言也有類似的功能),而導致權限沒被檢查而產生 security incident。

第二個是 os.makedirs() 所指定的 mode,依照說明看起來是 Python 改變行為了,變成只有最終的那個目錄照著設定:

In Python < 3.6, the folders A, B and C are each created with permission 700. However, in Python > 3.6, only the last folder C has permission 700 and the other folders A and B are created with the default permission 755.

這個設計就有點討厭了,我測了一下 umask 077 的情況下在 shell 下執行 mkdir a/b/c,所有的目錄都會是 700

第三個是串檔名的 os.path.join(),遇到 / 開頭的部份會把前面的部份都拔掉,也就是這樣:(嗯... 這樣搞的嗎 XDDD)

>>> os.path.join('var', 'lib', 'tmp', 'abc.png')
'var/lib/tmp/abc.png'
>>> os.path.join('var', 'lib', 'tmp', '/etc/shadow')
'/etc/shadow'

第四個就是蠻標準的 variable injection 了。

第五個算是每次處理 .zip 檔都很頭痛的問題,不限於 Python,因為壓縮檔裡面可能會有 ../../ 開頭這種看起來就很邪惡的檔案名稱,只要有安全意識的工程師都覺得處理起來很麻煩的東西...

第六個一看就是壞東西,想要用黑名單擋 injection 一定會有更多有創意的方法打進來。比較特別的是這邊有提到在 Python 裡 re.search()re.match() 的差異,這點真的是寫起來有時後會忽略掉的...

第七與第八個都是 Unicode 相關的問題,也是個超級麻煩的東西,大原則是先 normalize 再 escape,但還是有點見招拆招去看...

第九個也是類似的問題,沒有先 normalize 再確認,不過一般機器都會有 private ip address,還是可以試著打... 這個有機會用架構面去解決,像是用 MQ 架設 proxy service,隔出另外一區來跑;或是已經有 zero trust 的架構,內部網路也是要帶 token 跑來跑去,可以減少一些傷害...

第十個算是 Python 自己的歷史因素,早期同時吃 &; 當作 separator,而非只吃 &x-www-form-urlencoded (HTML spec 內定義的標準):

In Python < 3.7 the function urllib.parse.parse_qsl allows the use of the ; and & characters as separators for URL query variables.

查了一下官方文件可以看到 3.10 之後才變成 x-www-form-urlencoded

Changed in version 3.10: Added separator parameter with the default value of &. Python versions earlier than Python 3.10 allowed using both ; and & as query parameter separator. This has been changed to allow only a single separator key, with & as the default separator.

有些應該是有安全意識時會去翻 spec 看看就可以避開的,但有些就真的是蠻雷的 XDDD

Python 的 Black

Hacker News 上看到 Black 這個幫你處理 Python 程式碼的工具:「Black, the uncompromising Python code formatter, is stable (pypi.org)」。

Black is the uncompromising Python code formatter. By using it, you agree to cede control over minutiae of hand-formatting. In return, Black gives you speed, determinism, and freedom from pycodestyle nagging about formatting. You will save time and mental energy for more important matters.

然後從 Hacker News 上討論的情況看起來大家都覺得很不錯?好像可以看看能不能拿來用...

另外一個在討論的時候看到學到的東西,是 git blame --ignore-revs-file 這個功能,可以在 git blame 時濾掉某些 commit,剛好拿來過濾 reformatting commit:

Ignore revisions listed in file, which must be in the same format as an fsck.skipList. This option may be repeated, and these files will be processed after any files specified with the blame.ignoreRevsFile config option. An empty file name, "", will clear the list of revs from previously processed files.

Fork 自微軟的 Pyjion 專案的 Python 3.10 + JIT 方案

Hacker News 上看到「Pyjion – A Python JIT Compiler (trypyjion.com)」這個專案,也是一個想要透過 JIT 加速 Python 的專案:

Pyjion is a drop-in JIT Compiler for Python 3.10. It can be pip installed into a CPython 3.10 installation on Linux, Mac OS X, or Windows.

看了一下是從微軟的 Pyjion 專案 fork 出來的,原來的專案最後一次 commit 是一年前,而且專案也已經標示為 archived (read-only mode),但有留下轉移的說明,也就是上面提到的專案:

Development has moved to https://github.com/tonybaloney/Pyjion

可以看到大部分的效能都已經進入改善階段 (很多導入 JIT 的專案在初期時會先變慢):

跟其他的 JIT 方案相比,Pyjion 的目標是高度相容現有 Python 的程式,包括各種 extension,這點的確是在用 PyPy 這些軟體時的痛點沒錯...

看起來透過 pip 裝好後就可以直接 import 進來用,後續就會生效:

import pyjion; pyjion.enable()

另外提一下,翻 Hacker News 留言的時候翻到這個害我笑出來,有夠新 XD

zatarc 3 days ago | unvote | prev | next [–]

Pyjion requires: CPython 3.10 and .NET 6

.NET 6 Release: 19 hours ago (https://github.com/dotnet/core/blob/main/release-notes/6.0/6...)

... ok.

PyPy JIT 的改善

PyPy 這邊看到 JIT 的重大進展:「Better JIT Support for Auto-Generated Python Code」。

他們在 Tornado 上重製出來效能問題,後面也都是用這個例子在測試:

If you render a big HTML template (example) using the Tornado templating engine, the template rendering is really not any faster than CPython.

看起來上的 workaround 是在撞到 trace limit 時標記起來,之後再遇到時就可以跳進 special mode,接著處理下去避免浪費掉之前處理過的 trace:

After we have hit the trace limit and no inlining has happened so far, we mark the outermost function as a source of huge traces. The next time we trace such a function, we do so in a special mode. In that mode, hitting the trace limit behaves differently: Instead of stopping the tracer and throwing away the trace produced so far, we will use the unfinished trace to produce machine code.

效能可以看到改善很多:

看起來這個概念有打算在 3.8 的時候放進去:

The work described in this post tiny bit experimental still, but we will release it as part of the upcoming 3.8 beta release, to get some more experience with it. Please grab a 3.8 release candidate, try it out and let us know your observations, good and bad!

Django 的 template engine 不怎麼快,用 Jinja2 可能是一個方法,但既有的 project 如果有遇到 template engine 的效能問題,也許也可以翻看看 PyPy 解得如何...

在本機用 pip 直接安裝 PostgreSQL server

看到 PostgreSQL 官方站台上的介紹,可以直接用 Pythonpip 指令安裝 PostgreSQL server:「Install a local, non-root PostgreSQL Server with Python "pip"」,專案在「postgresql-wheel」這邊。

GitHub 上面的說明跑了一下,還真的可以惡搞... 這樣如果真的要在 CI 裡面跑的話也簡單很多了?只要能 pip 裝軟體就能跟你拼 XDDD

也省掉需要設定一些權限跑 Docker-in-Docker...

自架的 Google Search Proxy 伺服器專案:Whoogle Search

忘記在哪邊看到的連結,自架的 Google Search Proxy 伺服器專案:「Whoogle Search」,對應的 Hacker News 討論串也可以參考:「Whoogle Search: A self-hosted, ad-free, privacy-respecting metasearch engine (github.com/benbusby)」。

GitHub 上的分析可以看出來主要是 PythonFlask 寫的,然後說明就有提到是從 Google Search 撈資料,去掉所有可能可以被追蹤的項目:

Get Google search results, but without any ads, javascript, AMP links, cookies, or IP address tracking. Easily deployable in one click as a Docker app, and customizable with a single config file. Quick and simple to implement as a primary search engine replacement on both desktop and mobile.

目前最新版是 0.5.4,從他列出來的 Public Instances 找了一個是最新版的測試,看起來沒什麼大問題:「gslin - Whoogle Search」。

應該可以自己在台灣架一個起來玩看看?安裝方式看起來很多,因為是 Python-based 的套件,可以用 pipx 或是 Docker 裝起來跑,然後可以改寫「Press "g" to Google (DuckDuckGo)」(press-g-to-google-duckduckgo) 讓他可以設定要轉到哪個 Google Search Proxy...

Elasticsearch 的 Python 套件開始阻擋 OpenSearch 的伺服器了

Hacker News Daily 上看到的:「Official Elasticsearch Python library no longer works with open-source forks (github.com/elastic)」,連結所指向的是 GitHub 上的 pull request,在「Verify connection to Elasticsearch #1623」這邊。

講白了也就是 Elasticsearch 官方的 Python client 開始阻擋 AWS 主推的 OpenSearch

另外 AWS 這邊也出手,把本來的 client 都 fork 出來:「Keeping clients of OpenSearch and Elasticsearch compatible with open source」,這場戰爭還有得打...

快速產生 SQLite 資料的方式:一分鐘內產生十億筆資料

在「Towards Inserting One Billion Rows in SQLite Under A Minute」這邊看到作者想要在一分鐘內在 MBP 2019 上面寫 1B 筆資料進 SQLite,裡面有些方法還蠻值得玩一下的,這台 MBP 2019 機器的規格是:

The machine I am using is MacBook Pro, 2019 (2.4 GHz Quad Core i5, 8GB, 256GB SSD, Big Sur 11.1)

第一版是 Python 寫的,塞 10M 筆花了 15 分鐘:

In this script, I tried to insert 10M rows, one by one, in a for loop. This version took close to 15 minutes, sparked my curiosity and made me explore further to reduce the time.

加了五個 PRAGMA 的版本變成 100M 筆十分鐘:

The naive for loop version took about 10 minutes to insert 100M rows.

用批次處理則可以降到八分半:

The batched version took about 8.5 minutes to insert 100M rows.

再來是拿經典神器 PyPy 出來用,降到兩分半:

All I had to do was run my existing code, without any change, using PyPy. It worked and the speed bump was phenomenal. The batched version took only 2.5 minutes to insert 100M rows. I got close to 3.5x speed :)

接下來就是跳槽到 Rust 了,中間也有不少 tuning 相關的討論,但直接先跳到最後面好了... 最後 100M 只用了 33 秒:

I created a threaded version, where I had one writer thread that received data from a channel and four other threads which pushed data to the channel. This is the current best version which took about 32.37 seconds.

能用 PyPy 的地方還是可以考慮一下的...

用 Python 的 DuckDB 下 SQL 指令翻 Parquet 的資料

在「Querying Parquet using DuckDB」這邊看到 DuckDB 這個東西,裡面引用的文章是「Querying Parquet with Precision using DuckDB」,可以直接對 Parquet 格式的資料下 SQL 找資料。

先前好像有看到 DuckDB 但沒有太注意,剛剛再次看到,然後玩了一下還蠻有趣的。DuckDB 支援蠻多程式語言與資料格式,不過這邊文章拿 Python 與 Parquet 玩還蠻有趣的...

先把 Parquet 的範例資料抓下來,然後透過 pip 裝 duckdb:

cd /tmp; wget https://github.com/cwida/duckdb-data/releases/download/v1.0/taxi_2019_04.parquet; pip install -U duckdb

然後進到 Python 3 的互動界面:

>>> import duckdb
>>> print(duckdb.query("SELECT COUNT(*) FROM 'taxi_2019_04.parquet' WHERE pickup_at BETWEEN '2019-04-15' AND '2019-04-20'").fetchall())
[(1276565,)]

然後在範例裡面,檔名的部份還可以用 *,看了一下說明,底層是 glob 類的用法:

DuckDB supports the globbing syntax, which allows it to query all three files simultaneously.

文章裡有提到速度比 Pandas 快很多,不過我覺得這好像不太能這樣比,會拿 Pandas 出來的時候常常是其他用法,但至少看起來速度是個 DuckDB 在意的點。

不過反而馬上想到的是,之後處理 CSV 之類的檔案應該也會試看看 DuckDB...

Django 3.2 LTS

Django 3.2 LTS 出了:「Django 3.2 released」,對應的 release note 可以在「Django 3.2 release notes」這邊看到。

這個版本比較特別,一般版本提供大約 15 個月的支援,LTS 版本則提供 36 個月的支援,不過目前用起來的感覺還是鼓勵大家平常就要安排升級計畫,不然 3.2 升級到 4.2 鐵定是個超痛苦的過程。

昨天把 Django 3.1 的專案升級上 3.2 後,跑 test case 就遇到「Customizing type of auto-created primary keys」這邊描述的問題,目前先用 DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' 解,其他的倒是沒什麼問題...

幾個我覺得有趣的,像是 JSONL 格式:

The new JSONL serializer allows using the JSON Lines format with dumpdata and loaddata. This can be useful for populating large databases because data is loaded line by line into memory, rather than being loaded all at once.

然後不支援 PostgreSQL 9.5 與 MySQL 5.6 了:

Upstream support for PostgreSQL 9.5 ends in February 2021. Django 3.2 supports PostgreSQL 9.6 and higher.

The end of upstream support for MySQL 5.6 is April 2021. Django 3.2 supports MySQL 5.7 and higher.

很久前寫 Django 1.x,然後就荒廢很久,最近是從 3.0 開始寫,有些東西熟一點以後覺得怪怪的,之後要找時間測一些東西,修正對 Django 的用法。