pptr的一些缺陷和一个可靠的解决方案

前言

在之前的一篇博客:headless chrome & pptr初探中,我研究了puppeteer和pyppeteer的一些基本功能和常用技巧,并且总结了一下踩过的坑。 最近使用pyppeteer进行安全开发的过程中,发现使用pptr做漏洞扫描器有一些根本上的缺陷。 结果无数次折腾,终于想到了一种相对可靠的解决方案,在此记录。

pptr的缺陷

POST请求和headers修改极不方便

由于pptr在使用上尽可能地模拟chrome的用户操作,对POST请求和headers修改也和chrome一样非常麻烦。 现在要发送post请求或更改headers,我们必须先用page.goto()发送一个GET请求,然后使用RequestInterception拦截该请求,再使用overrides将请求改为POST方法或更改headers,完整代码如下:

1
2
3
4
5
6
7
8
9
10
page.setRequestInterceptionEnabled(true);
page.on('request', request => {
let overrides = {
method : 'POST',
postData : 'my=data',
headers : headers,
}
request.continue(overrides)
});
page.goto("http://www.example.com")

这种做法是及其不优雅的。 而且pptr也不打算支持post功能,详情可参考https://github.com/GoogleChrome/puppeteer/issues/1062

无法对response做任何更改

这个问题和上面的问题类似,都是因为pptr不打算像Burpsuite那样完全控制出入流量,而是模拟正常用户。 但有时候我们会希望在渲染DOM树前就插入一段js代码来变更页面行为。 这个功能pptr也不打算支持,详情可参考https://github.com/GoogleChrome/puppeteer/issues/599

headless模式无法使用插件

上一篇博客中我介绍了使用chrome插件来阻止页面跳转,遗憾的是,这种方法只有在headless: false的时候才能使用,当开启headless模式时,插件就不会被加载了。 详情可参考https://github.com/GoogleChrome/puppeteer/issues/823

pyppeteer的一个bug

如果无法使用chrome插件,还有什么办法限制浏览器跳转呢?实际上,window中有个事件window.onbeforeunload可以做到在页面跳转时弹窗提醒。 我们再将page设置为默认拒绝所有的弹窗,页面便不会跳转了。 代码如下:

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
const puppeteer = require('puppeteer');

async function main() {
const browser = await puppeteer.launch({
headless: false,
args: ['--ignore-certificate-errors',
'--allow-running-insecure-content',
'--disable-xss-auditor',
'--no-sandbox',
'--disable-setuid-sandbox']
});
const page = await browser.newPage();
await page.on('dialog', (dialog)=>dialog.dismiss())
await page.goto("http://www.example.com")
await page.evaluate(() => {
window.onbeforeunload = function(event) {
return "stop navigating";
};
});
await page.evaluate(() => {
location.href="about:blank";
});
await page.waitFor(1000);
await browser.close();
}

(async () => { await main() })()

但是,上述代码在pyppeteer下不能阻止跳转。 经过与作者联系,确认为一个bug。 详情可参考:https://github.com/miyakogi/pyppeteer/issues/120

一个可靠的解决方法

从上面的例子中,我们可以看到,pptr在对请求和响应的控制存在很大的问题。 那么我们能不能转变思路,不使用puppeteer来处理请求和响应,只用它渲染DOM树呢?翻了翻puppeteer的api,我找到了这样一个方法:

1
2
3
page.setContent(html)
- html <string> HTML markup to assign to the page.
- returns: <Promise>

该方法接受一个html字符串,并渲染到指定页面上。 于是一套结合requests模块和pptr模块的控制流程顺势而生:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
import asyncio
from pyppeteer import launch

async def main():
browser = await launch({"headless": False,
"args": ['--ignore-certificate-errors'
'--allow-running-insecure-content',
'--disable-xss-auditor',
'--no-sandbox',
'--disable-setuid-sandbox']})
page = await browser.newPage()
r = requests.get('http://www.example.com') # 在这里修改请求方法和内容
html = r.text + '<script>window.onload=function(){window.onbeforeunload = function(event) {return "stop navigating";}</script>' # 在这里修改响应内容
page.setContent(r.text) # 在这里渲染页面,并执行后续交互操作
await browser.close()

asyncio.get_event_loop().run_until_complete(main())

上述代码中我们使用requests模块来处理请求,因此我们能够完全控制请求方法、 请求头以及响应内容。 我们可以将响应内容插入js代码来限制跳转,再交给pptr渲染。 渲染完毕之后,我们依然可以使用pptr带来的便捷操作以及强大的调试工具。

后话

从整个开发过程中的发现问题、 思考并解决问题,我想最重要的是理解每种工具的长处和短处分别是什么。 学会扬长避短才是我们工程师最大的价值。

参考资料