HTML元素属性测试总结,包含DOM,CSS,javascript 与 AJAX
准备工作:使用firebug插件支持的console.log()方法(函数)来动态调试HTML文档中的javascript代码。
1。比较console.log() 与 document.write() 在调试时的功能差异:
如果直接以浏览器打开HTML文档,其中的console.log()方法是不会获得执行的,因为浏览器并不直接支持这个方法,需要在浏览器(如Chrome)的开发者工具或者插件(如Firebug)中的控制台模块,以及console模块内,才能执行这个方法;
而 document.write() 方法不需要额外的开发工具或插件要求,浏览器可以直接执行并且将结果回写至HTML文档内。
两者的运行环境和效果对比请看下面一系列的截图:
其它浏览器对于 console.log() 与 document.write() 的支持情况与 FireFox 类似,基本上都是需要在开发者工具中,才能使用 console.log(),这里就不截图了。
下面的图片着重展示如何在 Firebug 中使用 console.log() :
2。除了console.log() 外,还可以使用其变体,如 console.info(),console.debug(),console.warn(),console.error() 等等,需要明确指出使用的是哪一个变体,不能省略,否则开发工具或插件默认会使用 console.log(),例如:
console.warn(alert(navigator.userAgent))
这些变体方法之间的差别在于显示代码执行结果信息的详细程度与图标样式,但总体而言差别不大。
另外一个有用的方法是 console.dir(),如果传递给它的参数是用单引号或者双引号包含的字符串,它可以“逐字符”在控制台窗口输出这些字符;如果不是字符串,那么将输出该参数的值,请参考下面2张图片了解差异:
需要注意的是,仅 Firebug 对 console.dir() 的支持程度较好,Chrome 的开发者工具无法完整地实现这个方法。但是后者的优点是,在控制台输入脚本代码时,可以自动列出所有用户想要键入的候选对象,即类似搜索引擎与软件开发 IDE 中的自动完成,补全功能:
当直接修改 HTML 文档中的内容时,可以使用“”字符串对,来注释文档中任意元素,这会导致浏览器在解析的时候忽略被注释的内容;
要注释 script 标签中的多行脚本代码,则需要使用 C 语言风格的注释语法,即
“/*”与“*/”字符串对。
3。比较 input 元素的 type 属性几种常见的值
input 元素经常被包含在 HTML 表单,即 form 元素内部,用来获取各种形式的用户输入数据(帐户密码),甚至可以支持用户上传文件(type 属性值为 file 时),如下所示:
XssPayloadTest
使用不同类型的浏览器打开这个页面,观察它们是如何渲染,解释 type 属性的各种取值的:
关于通过 HTML 表单上传文件的实际应用,参考下面的例子:
一些论坛在发帖的时候,可以在帖子中插入附件或图片,这就是依赖于 HTML 表单的提交文件功能来实现上传的,以看雪论坛为例,在编辑帖子的富文本框顶部,有一列工具栏,点击其中笑脸右侧的“附件管理”按钮,就可以打开独立的浏览器窗口选择要上传的附件。这里我们使用 FireFox 的 Firebug 插件进行测试,在打开的浏览器窗口右上方点击瓢虫图标,从而对当前页面激活 Firebug:
在 Firebug 显示的管理附件页面的 HTML 源码中,依序展开 body 元素(节点)-> div 元素,即可看到 div 元素内部的 form 表单元素,使用 HTTP POST 方法提交,其 action 属性的值预设了负责处理表单提交的 URL 一部分,也就是 “newp_w_upload.php?do=manageattach&p=”,后面我们会使用 BurpSuite Proxy 的拦截 HTTP 请求功能验证,当用户点击右侧的“上传”按钮时,的确以 POST 方法请求这个 URL ,并且还会在请求体中携带一些其它的参数提交;
表单元素的 enctype 属性值 multipart/form-data (多部分表单数据),是表单能够上传文件的关键之一,它必须结合 POST 方法才能工作,因为处理用户输入文本数据的普通表单,在使用 POST 方法提交时,默认使用 application/x-www-form-urlencoded 对输入数据进行编码,也就是将数据组织成“字段名/字段值对”的形式,每个配对之间用 & 符号连接,当任一对字段中出现不可打印或特殊的 ASCII 字符,unicode 字符时,对其进行百分号 URL 编码,这就是 x-www-form-urlencoded 名称的由来。
编码后的数据被放在 HTTP 请求体中发送(作为对比,HTTP GET 方法将用户输入的数据放在请求头部第一行的 URL 后的查询字符串中发送)。
而处理用户上传文件的表单,则需要显式设置 enctype="multipart/form-data",用于告诉浏览器要处理的是文件(图片,视频等)。
后面会通过 BurpSuite Proxy 拦截到浏览器发出的“多部分表单数据”请求,详细介绍 multipart/form-data 这种数据提交编码格式。
回到管理附件页面的 HTML 源码中,继续展开 form 元素,可以在其内部看到许多隐藏的 input 元素(input type="hidden"),浏览器不会在页面中显示属性为隐藏的 input 元素,但我们还是可以通过一些手段,例如这里的 Firebug 插件,查看和修改其内容,细心的你也许已经注意到了,这些隐藏的 input 元素的预设值,大部分与当前管理附件页面 URL 中的查询字符串中的值相同:
观察管理附件页面的结构,通过“浏览”按钮选择文件并上传,其顶部有一个字符串“从您的电脑上传文件”,将该字串复制到 Firebug 的搜索框中后回车,可以定位字串在 HTML 源码中的位置;从这个位置继续向下观察,我们找到了实现文件上传的“浏览”按钮的源码,也就是 6 个连续的 type 为 file 的 input 元素,前面已经介绍过当浏览器碰到 input type="file" 的时候,将显示一个按钮控件,让用户从本地硬盘中选择文件上传,这就是表单实现上传功能的第二个关键。
继续在页面源码中向下探索,找到一个 input 元素的 type 属性值为 submit;value 属性值为“上传”,这对应浏览器渲染的页面中的“上传”按钮。
注意其 onclick 属性的值为 return verify_upload(this.form);
这表明在按下该按钮时执行的函数为 verify_upload ,该函数定义在 body 元素内部,位于 div 元素的前面,大致的功能是检查并提示用户选择文件上传(如果用户没有选择文件就按下“上传”按钮):
接下来,使用 BurpSuite Proxy 拦截浏览器在处理通过 HTML 表单上传文件时,向服务器发送的 HTTP 请求,以及对方返回的 HTTP 响应,这样我们就可以深入理解 multipart/form-data (多部分表单数据)的内幕。
限于篇幅,关于 BurpSuite 的下载安装配置这里不作介绍,各位可以自行搜索相关教程。首先在看雪论坛的附件管理页面中,通过“浏览”按钮实际选择一个要上传的测试图片,加载目标图片后,还不要点击“上传”按钮,因为我们的重头戏就是使用 BurpSuite 拦截点击“上传”按钮时,浏览器发出的 HTTP 请求。
在保持上传页面窗口打开的状态下,通过点击 FireFox 的其它任意标签页或者独立窗口(或者原始的帖子编辑窗口)右上角的选项 -> 高级 -> “网络”选项卡,点击“连接”栏目右侧的“设置”按钮,选择“手动配置代理”单选框,配置 FireFox 使用监听在 127.0.0.1:8080 的 BurpSuite 代理访问互联网(BurpSuite 启动后默认监听在上述地址端口):
配置好浏览器使用代理后,启动 BurpSuite ,切换到 proxy 选项卡下的 Options 子选项卡,默认情况 BurpSuite 仅拦截客户端发出的 HTTP 请求,因此需要在
Intercept Server Responses 栏目中,勾选 “Intercept responses based on the following rules”方框,保持下面的拦截规则为默认状态即可,这样就能够拦截服务器端返回的 HTTP 响应了:
切换到 intercept 选项卡中,确保从左边开始数起,第三个按钮的状态为“intercept is on”,然后点击附件管理页面的“上传”按钮,此刻就能拦截到浏览器发起的 HTTP 请求,注意,这个请求还未到达服务器,如果点击左边第一个 “Forward”按钮,请求才会被转发至服务器,而在此之前,我们可以任意修改,编辑请求头部和主体的所有字段,内容,甚至第二个按钮“Drop”可以丢弃该请求,不转发给服务器;最后一个按钮“Action”提供了更多在***测试中实用的操作选项,各位可以自行测试。拦截到的 HTTP 表单上传文件请求数据包如下所示:
通过上面的介绍,各位对 HTML 表单上传文件的机制应该都了解清楚了,也可以尝试对其它站点的文件上传功能进行检测,顺便提一下,51cto 博客的文件上传不是基于 HTML 表单实现的,而是借助 Flash 浏览器插件的网络功能实现的,不过同样可以用 BurpSuite 拦截点击上传图片控件时,Flash 浏览器插件发起的 HTTP 请求或者通信流量,其原因在于,51cto 博客的服务器端返回的是基于 javascript 构建的文件上传界面的 HTML 文档,其中虽然没有用到表单元素,但是同样是使用 HTTP POST 方法提交数据,而且其 Content-type 请求头的值也是 multipart/form-data,由此产生的流量可以被 BurpSuite 拦截。各位可以自行测试研究,然后对比两者间的异同。
总结:51cto 博客使用 POST 上传图片或者文件时,在请求体中的数据类型为 multipart/formdata,携带了实际表示图片文件的字节序列;服务器返回的响应体中的数据类型为 JSON ,包含一些通知客户端上传成功的轻量级信息(图片存储的 URL,名称等),浏览器的 javascript 解释引擎能够识别 JSON 数据并更新页面特定区域的数据或状态(例如在上传图片的界面右下方显示一个绿色的勾,就是用来响应 JSON 数据中的 "state":"SUCESS" 参数名/值对),在使用 AJAX 时,多数情况下 XMLHttpRequest 对象中也能保存由服务器返回的 JSON 数据。
3。 关于执行页面中 script 标签内代码和构建 DOM树的优先顺序
考虑下面的代码:
XssPayloadTest
在第8行的 script 标签内部,取得 ID 为 a 的元素节点(即 div 元素)内部的 HTML 代码,然后在弹出的提示框中显示。而实际上,用三大浏览器测试,都不会弹出显示 div 元素内部 HTML 代码的提示框,原因在于,碰到 script 标签时,浏览器们直接调用 javascript 解释引擎执行其中的脚本代码,而此时 DOM 树中的节点还未构建完整:div 元素是在浏览器碰到第9行代码时,才添加到 DOM 树中的,而第8行的代码不等待文档的 DOM 树构建完成,就先读取还未创建的 ID 为 a 的 div元素,这会导致浏览器提示“无法读取null(不存在)元素的innerHTML属性”:
其解决办法之一,将要操纵页面 DOM 节点的脚本代码放在最后,这样一来,在浏览器碰到 script 标签之前,所有的 DOM 节点都已经构建完毕:
XssPayloadTest
浏览器先通过 HTML 解析器构建 div 元素,然后脚本代码再操纵该元素,这样就不会有问题了:
解决方案之二,把要执行的脚本代码放在 body 元素的 onload 属性值内, onload 事件在页面加载完,DOM树完整构建后,才触发定义的脚本代码,因此可以读写 div 元素,这样,即便脚本代码的位置在 div 元素的前面,也不会有问题:
XssPayloadTest
使用 onload 属性在页面加载完成时触发事件,执行自定义的脚本代码,这种方法虽然很方便,但是也有缺点: onload 事件在“页面加载完才触发”,这意味着如果页面有大量的图片,视频等多媒体资源,需要等待所有这些都加载完毕,才执行脚本代码,如果执行的代码需要快速响应用户,无疑会降低其使用体验;另一方面,浏览器构建完整 DOM 树后,才开始加载图片,视频资源,而前者所花费时间远小于后者,因此可以在 DOM 树构建完整后,立即执行自定义代码,就避免了使用 onload 造成用户等待的情况发生。
这可以通过添加事件监听器 DOMContentLoaded 来实现:
XssPayloadTest
在上面这个文档中,利用 document 对象的 addEventListener 方法设置在当前文档的 DOM 树加载解析完成后(DOMContentLoaded),将触发其第二个参数定义的脚本并且执行,该脚本读取 ID 为 a 的元素的内部 HTML 代码,并且在提示框中显示“div 内的 HTML 是 a 节点”,因此虽然这段脚本在 ID 为 a 的 div 元素创建前执行,但是 addEventListener 方法设置了只有 DOM 树完整构建后,才读取 div 元素。使用事件监听器,不仅可以正确读写操作元素,而且还避免了等待图片资源加载浪费的时间,可谓一举两得:
有一点需要注意:上面代码中,addEventListener 方法的第二个参数是一个“匿名函数”,其在调用时定义;当在同一个HTML文档的 javascript 代码中使用事件监听器指定了多个用户自定义函数时,就需要对后者显式取名,而不能像第一个例子那样使用匿名,否则无法分辨究竟调用的是哪一个函数:
XssPayloadTest
上面文档中,script 标签内部用一个独立的代码块定义了 bar 函数,然后使用addEventListener 方法设置在用户点击页面任意位置时(触发 click 事件),执行bar 函数,弹出提示框:
最后,可以检测浏览器是否支持 document.addEventListener(),就当前的主流浏览器而言,均支持该方法,具体而言,在浏览器的控制台中,输入document.addEventListener 后回车,如果浏览器支持该方法,则会输出其函数原型,或者调用该函数时传递的参数状态,调用者,函数长度(仅 IE 的开发者工具支持后3个特性),反之,当浏览器不支持一个对象或者方法时,则在控制台输出“undefined”(未定义)信息,前端开发时,均可以用这个方法来检测特定浏览器对于特定对象方法的支持程度:
4。HTML 文档的跨平台支持:根据不同的浏览器类型,执行不同的代码块
由于各种浏览器对于新的 HTML5 规范,以及 DOM 的支持,实现程度不一致,为了保证自己所编写的文档,以及其中的脚本代码能够在所有主流浏览器中正确运行,一种解决办法是:先判断浏览器类型,然后执行相应浏览器支持的代码块。实现这个功能的关键,在于浏览器的用户代理字串值,即 navigator.userAgent
实现逻辑如下:
XssPayloadTest
这是一个非常简单的先判断然后输出用户使用的浏览器类型的页面。
第8行直接在当前页面输出打开该页面的浏览器的用户代理字串;第11~14行分别查找各种浏览器的用户代理字串中,能够唯一识别浏览器类型的关键词,这是由于,在早期的“浏览器世界大战”中,许多 web 页面的特定内容仅支持 Netscape Navigator 浏览器(网景公司的产品,其 “Mozilla” 字串就是它首创的,含义为“Mosaic Killer”,即浏览器鼻祖 Mosaic 的杀手),而其它品牌浏览器为了提高自身市场份额与竞争力,也在其用户代理中添加了“Mozilla”字串,用于告诉目标站点:“我是与 Mozilla 兼容的浏览器”,如此一来,站点就会返回原本仅响应给 Mozilla 浏览器的页面内容,而这个内容通常是比较丰富的;
最终效果就是, IE 浏览器的用户访问某站点获得的内容与 Netscape Navigator 浏览器的用户获得的内容一致,因为 IE 在它的用户代理字串中,添加了“Mozilla”字串。
其它浏览器相继模仿这个做法,导致通过“Mozilla”字串根本无法实际识别浏览器类型,所以,必须找到用户代理字串中,每个浏览器都不一样的关键词,这就是第11~14行代码的工作,例如第11行查找用户代理字串中的“Trident”,后者是 IE 浏览的 HTML 渲染引擎内核代号,然后保存在一个变量中,第15~16行代码通过判断这个变量的值来输出用户使用的是 IE 浏览器(在实际开发时,应该把这里的内容替换成仅 IE 浏览器支持的页面代码,其它类型浏览器以此类推)。
另一方面,当前的 chrome 与 Opera 浏览器的用户代理字串中,都包含了“Chrome”关键词,所以仅凭它无法区分这2个浏览器,需要检查在包含“Chrome”关键词的用户代理字串中,是否含有“OPR”字串,如果有,则能识别出 Opera 浏览器,反之,则为 chrome 浏览器;而这就是第17~22行的嵌套 if-else 语句的工作。顺便提一下,这2个浏览器的 HTML 解析引擎内核都源于苹果公司开发 Safari 浏览器时的内核 WebKit;google 公司在 WebKit 的基础上开发出新的内核称为 Blink;这2个浏览器厂商“饮水思源”的做法导致你在使用这2个浏览器打开上述文档时,看到了各自的用户代理字串都包含“AppleWebKit”:
我在第25~27行就停止了判断浏览器类型,你也可以继续添加判断其它浏览器的 else if 语句块,例如 Safari,以及国产的 360 浏览器,这需要预先找出能唯一标识它们的用户代理字串片断,就如第11~14行代码所做的一样,各位可以自行测试。