浏览器中的Python:PyScript技术详解与应用实例

本文深入探讨了PyScript框架,它允许在浏览器中直接运行Python代码,并展示了如何与HTML和JavaScript集成,包括使用外部库、构建安全测试工具和生成TOTP验证码等实际应用案例。

PyScript – 浏览器中的Python及其应用

几天前,Anaconda项目宣布了PyScript框架,该框架允许直接在浏览器中执行Python代码。此外,它还涵盖了与HTML和JS代码的集成。

在浏览器中执行Python代码并不是新事物;pyodide项目长期以来一直允许这样做(通过将Python编译为WebAssembly),但这里的新颖之处在于与浏览器生态系统的其余部分的集成。得益于PyScript,我们可以轻松地从pip仓库直接包含许多模块:所有用“纯”Python编写的模块都应该工作,一些需要本地代码的模块也被重新编译为WebAssembly。

Python有一个非常大的标准库,以及许多优秀的外部库,因此这个项目开辟了许多有趣的机会,可以快速构建简单的工具,如安全测试,或允许展示一些培训概念。

PyScript基础

让我们看一个用PyScript编写的非常简单的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!doctype html>
<html>
  <head>
    <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
  </head>
  <body>
    <py-script>
from js import alert
alert("Securitum says hi!")
    </py-script>
  </body>
</html>

如您所见,我们只需要加载脚本,然后就可以在一个特殊的<py-script>标签中编写Python代码。在上面的例子中,我使用了PyScript内置的js模块,它允许我们直接引用JavaScript函数。所以,正如您所猜测的,加载上述页面后,您将看到一个带有文本“Securitum says hi!”的警报。

与JS的更多交互

让我们尝试与JS进行更多交互。我将编写一个简单的脚本,使用Python的secrets模块生成随机令牌。这些令牌将在按下按钮时生成,并显示在另一个HTML元素中。以下是一个示例实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!doctype html>
<html>
  <head>
    <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
  </head>
  <body>
    <button>Click me</button>
    <div>The random token is: <b id=token></b></div>
    <py-script>
from js import document,alert
from pyodide import create_proxy
import secrets

def onclick(ev):
  pyscript.write("token", secrets.token_hex(16))
  
button = document.querySelector("button")
button.addEventListener("click", create_proxy(onclick))
    </py-script>
  </body>
</html>

如果我们希望JS代码能够执行在PyScript中定义的函数,我们需要通过调用create_proxy来包装它。PyScript提供了pyscript.write方法,允许您直接将HTML分配给具有给定ID的元素。因此,不需要引用document.getElementById(尽管这是可能的)。

从pip导入模块

虽然使用标准库中的模块以正常的导入结束,但如果我们想从pip仓库加载模块,则需要采取一些额外的步骤。

具体来说:必须在HTML中添加一个特殊的标签<py-env>。在该标签中,我们定义了外部模块的列表。例如,如果我们想导入primefac,我们必须事先使用该标签:

1
2
3
<py-env>
 - primefac
</py-env>

在接下来的代码中,基本的import primefac将起作用。

primefac是一个用于分解数字的有用库。因此,让我们编写一个非常简单的脚本,使用它来分解用户提供的数字。代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!doctype html>
<script defer src="https://pyscript.net/alpha/pyscript.js"></script>
<py-env>
  - primefac
</py-env>
<p><label>Enter an integer in the range from 1 to 10000: <input id=n></label></p>
<p>The prime factors are:: <span id=factors></span></p>
<py-script>
import primefac
from pyodide import create_proxy

input = document.querySelector("#n")
def oninput(ev):
  num = int(ev.target.value)
  num = min(num, 10000)
  num = max(num, 1)
  factors = list(primefac.primefac(num))
  pyscript.write("factors", factors)

input.addEventListener("input", create_proxy(oninput))
</py-script>

在学习上述示例的基础知识后,我们可以尝试以更实际的方式使用PyScript。

示例#1:测试bleach清理器

Bleach是Mozilla开发的一个Python库,用于从恶意元素或允许利用XSS漏洞的属性中清理(净化)HTML代码。就个人而言,我非常喜欢测试清理器,其中最方便的方法之一是编写一个代码,在按下下一个键后立即执行清理。

使用PyScript,编写这样的环境非常简单。

示例#2:密码学(+生成QR码)

在Python中,有一个非常有用的库叫做cryptography,它允许您执行基本上任何密码学操作(加密、签名、生成一次性代码、哈希等)。它不是用“纯”Python编写的,而是那些专门为pyodide(以及PyScript)编译的库之一。

作为培训的一部分,我决定尝试使用这个库,并用它编写一个Google Authenticator模拟器。我可以轻松地做到这一点,因为cryptography有一个现成的模块,可以使用TOTP算法生成一次性代码。

所以我的计划是让代码能够:

  • 为Google Authenticator生成随机密钥。
  • 生成可以轻松由Authenticator导入的QR码。
  • 生成更多一次性代码(应与应用程序中的代码相同)。

让我们分别看看这些元素。

第一步,Authenticator中的密钥长度为80位(10字节),并且是Base32编码的。例如,它们看起来像这样:7NWLVFIDJSXOVSSV。生成它们只需要使用密码学安全的伪随机数生成器,例如来自secrets模块。

1
2
3
import secrets
def get_random_key():
  return secrets.token_bytes(10)

在代码中,它不会将数据编码为Base32,因为稍后cryptography库会处理。

第二步,生成QR码。它将需要使用另一个库,即:qrcodepillow(被qrcode用来生成图像)。

有趣的是,PyScript提供了轻松显示在我们代码中生成的图像的能力,但——不幸的是——目前这段代码有一个错误,无法正确显示它们。幸运的是,通过定义我们自己的render_image方法来生成正确的图像,很容易修复。

PyScript代码块也被解释为,我们代码中的最后一行被视为该块返回的值。因此,如果在最后一行我们简单地放置对图像的引用——PyScript将识别出我们想要显示这个图像。

以下是一个显示带有文本的QR码的示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<py-env>
  - qrcode
  - pillow
</py-env>
<py-script>
import qrcode
import base64 

# 修复原始损坏的render_image方法
def render_image(mime, value, meta):
  data = f"data:{mime};base64,{base64.b64encode(value).decode('utf-8')}"
  return f"\u003cimg src='{data}'\u003e"

img = qrcode.make("Witaj Sekuraku!")

# 在最后一行代码中返回对图像的引用
img
</py-script>

当然,为了让Google Authenticator接受QR码,它必须包含适当的内容。正如我们从文档中了解到的那样,格式如下:

然而,我们不必自己准备这样的URL格式,因为cryptography有一个名为get_provisioning_uri的方法。

因此,让我们继续最后一步,即生成一次性代码。以下是一个代码示例,它将为给定时间和给定密钥生成另一个一次性代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import os
import time
from cryptography.hazmat.primitives.twofactor.totp import TOTP
from cryptography.hazmat.primitives.hashes import SHA1

# 我们正在生成密钥
key = os.urandom(20)
# 我们定义TOTP的长度为6个字符,并且每30秒重新生成一次
totp = TOTP(key, 6, SHA1(), 30)
time_value = time.time()
# 我们为给定时间生成代码
totp_value = totp.generate(time_value)

一旦我们有了totp对象,我们就可以对其执行前面提到的get_provisioning_uri方法。

因此,让我们把所有东西放在一起。代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<div style="border: 1px solid black;padding: 1em; font-family:monospace">
<p><strong>TOTP key</strong>: <span id=otpkey></span></p>
<p><strong>One-time code</strong>:</p>
<p><span style=font-size:xxx-large;font-weight:bold id=otpotp>123</span></p>
<p><strong>Next code in</strong>: <span id=next_code_in></span>s</p>
<p><button id=newotpkey>Generate a new key</button></p>
<div><img id=otpqr style=max-width:320px></div>
<py-env>
  - qrcode
  - pillow
  - cryptography
</py-env>
<py-script>
import qrcode
import base64
import secrets
from cryptography.hazmat.primitives.twofactor.totp import TOTP
from cryptography.hazmat.primitives.hashes import SHA1
from js import setInterval, document
from pyodide import create_proxy
from io import BytesIO

OTP_LENGTH = 6
OTP_INTERVAL = 30

state = {
  "key": b"",
  "otp": "",
  "totp_object": None,
  "qrcode": None
}

# 密钥重新生成和TOTP对象的创建
def regenerate_key(arg=None):
  global state
  state["key"] = secrets.token_bytes(10)
  state["totp_object"] =  TOTP(state["key"], OTP_LENGTH, SHA1(), OTP_INTERVAL, enforce_key_length=False)
  uri = state["totp_object"].get_provisioning_uri("PyScript", "Securitum")
  img = qrcode.make(uri)
  buf = BytesIO()
  img.save(buf, format="png")
  imgdata = f"data:image/png;base64,{base64.b64encode(buf.getvalue()).decode('utf-8')}"
  state["qrcode"] = imgdata
  
  generate_otp()

def generate_otp():
  global state
  state["otp"] = state["totp_object"].generate(time.time())

qrcode_image = document.getElementById("otpqr")

# 每秒调用一次以更新HTML状态的函数
def update_html():
  global state
  generate_otp()
  next_code_in = OTP_INTERVAL - time.time() % OTP_INTERVAL
  pyscript.write("otpkey", base64.b32encode(state["key"]).decode('utf-8'))
  pyscript.write("otpotp", state["otp"].decode('utf-8'))
  pyscript.write("next_code_in", round(next_code_in))
  qrcode_image.src = state["qrcode"]

newotpbutton = document.getElementById("newotpkey")
newotpbutton.addEventListener("click", create_proxy(regenerate_key))

setInterval(create_proxy(update_html), 1000)

regenerate_key()
update_html()
</py-script>
</div>

检查脚本是否正常工作的最佳方法是将QR密钥导入Authenticator,并验证它是否生成与脚本相同的代码。如下图所示——看起来确实如此!

示例#3:sqlite

我想在本文中展示的最后一个示例将使用sqlite。sqlite处理程序包含在Python标准库中,并且可以从PyScript级别使用,因此我认为它可以用作培训中SQL注入的演示。

我将构建一个非常简单的脚本,该脚本:

  • 将创建sqlite数据库。
  • 在数据库中创建两个表;一个包含产品数据,另一个包含用户数据。
  • 将提供一个界面,用户可以在其中键入要搜索的短语。这将容易受到SQL注入的攻击。
  • 目标是从第二个表中提取数据。

事不宜迟,让我们直接看代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<p><label>Enter the phrase: <input id=searchphrase oninput="search_sql(event.target.value)"></label></p>
<p><strong>Executed SQL:</strong></p>
<pre wrap id=sqlsql></pre>
<p><strong>Result:</strong></p>
<pre wrap id=sqlresult></pre>
<py-script>
import sqlite3
from js import window, document
from pyodide import create_proxy

# 创建内存中的sqlite数据库
con = sqlite3.connect(":memory:")
cur = con.cursor()
products = [
  ("Product 1", "Description 1", 12),
  ("Product 2", "Description 2", 33),
  ("Product 3", "Description 3", 34),
]
users = [
  ("mb", "very-secret-password"),
  ("ms", "even-more-secret-password")
]

cur.execute("CREATE TABLE products (name, description, amount)")
cur.execute("CREATE TABLE users (name, description)")

cur.executemany("INSERT INTO products VALUES (?, ?, ?)", products)
cur.executemany("INSERT INTO users VALUES (?, ?)", users)

sqlsql = document.getElementById("sqlsql")
sqlresult = document.getElementById("sqlresult")

def search(query):
  sql = f"SELECT * FROM products WHERE name LIKE '%{query}%' OR description LIKE '%{query}%'"
  sqlsql.textContent = sql
  try:
    cur.execute(sql)
    result = cur.fetchall()
  except Exception as ex:
    result = str(ex)

  sqlresult.textContent = result
  
window.search_sql = create_proxy(search)
</py-script>

当然,如果这些类型的脚本实际上要用于培训,它必须看起来更好,并向用户呈现更多信息,但这个示例本身应该已经显示出它可以多么容易地准备。

总结

PyScript是一个有趣的新项目,允许您直接在浏览器中执行自己的Python代码。目前,它仍处于早期阶段,不难找到无法工作的组件,但它有很大的潜力。从我的角度来看——在培训方面,以及使用广泛的Python标准库准备简单工具以方便日常活动方面,它看起来特别有趣。

— Michał Bentkowski

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计