返回介绍

3.1 HTTP 与会话管理

发布于 2024-10-10 22:16:30 字数 28228 浏览 0 评论 0 收藏 0

为什么要学习 HTTP

Web 应用的安全隐患有些源于网络的固有特性。在 Web 应用中,哪些信息容易泄漏,哪些信息容易被篡改,如何才能保证信息安全?正是因为开发人员缺乏这些知识,才会在开发时埋下安全隐患。为了理解诸如此类源自 Web 特性的安全隐患,就必须要掌握 HTTP 和会话管理的相关知识。而这也是本节要讲述的内容。

最简单的 HTTP

在正式开始前,先来体验下最简单的 HTTP 吧。31-001.php 中有如下 PHP 代码。这段脚本的功能为显示当前时间。

代码清单 /31/31-001.php

<body>
<?php echo htmlspecialchars(date('G:i')); ?>
</body>

访问 http://example.jp/31/ 的菜单(以下写作“/31/ 菜单”),点击“31-001: 当前时间”链接(图 3-1),就可以在虚拟机上执行这段脚本了。

图 3-1 /31/ 菜单

执行结果如图 3-2 所示。

图 3-2 显示时间脚本

与此同时,在后台,浏览器会向服务器发送 HTTP 请求(HTTP Request),而收到浏览器请求的服务器则会向浏览器发回 HTTP 响应(HTTP Response)(图 3-3)。

图 3-3 HTTP 的请求与响应

  • 使用 Fiddler 观察 HTTP 消息

    我们可以使用 Fiddler 来观察 HTTP 消息。启动 Fiddler 后,在 IE 浏览器上刷新刚才的页面。这次,浏览器和服务器之间的通信经过了 Fiddler,所以在 Fiddler 上能够看到 HTTP 的通信内容。

    图 3-4 通过 Fiddler 显示 HTTP 通信

    为了让 Fiddler 显示 HTTP 的通信情况,如图 3-4 所示,选择界面上方的“Inspectors”-“Raw”标签以及界面中间的“Raw”标签。然后,在界面左侧选择 31-001.php 请求。

    图中右侧红框中的内容就是浏览器与 Web 服务器之间互相传递的消息。下面,让我们来具体看一下这些内容。

  • 请求消息

    Fiddler 界面右侧上半部分显示的内容,是浏览器向服务器发出的请求,被称为请求消息(Request Message)。

    请求消息的第 1 行被称为请求行(Request Line),相当于浏览器下达给服务器的命令。请求行由请求方法、URL(URI)和协议版本组成,它们之间以空格相隔(图 3-5)。在 Fiddler 界面中,请求行上显示的是包含了 Scheme(协议)和主机名(FQDN,全称域名)的绝对路径的 URL,这是因为请求经过了代理(Fiddler)的缘故,而通常情况下只会显示相对路径的 URL。

    图 3-5 请求行

    HTTP 的请求方法除了 GET(取得资源)以外,还有 POST 和 HEAD 等。GET 和 POST 与 HTML 中 form 元素的 method 属性指定的值相同。关于 POST 方法后面还会讲述。

    请求消息的第 2 行及以后的内容被称为请求头信息(Header),其格式为名称与值以冒号相隔。图 3-4 中显示了很多请求头信息,但其中只有 Host 是必需的 1 。Host 表示接收信息的主机名(FQDN)和端口号(80 时可以省略)。

  • 响应消息

    图 3-4 右侧的下半部分显示的是从 Web 服务器返回的内容,被称为响应消息(Response Message)。如图 3-6 所示,响应消息包含状态行、响应头信息和响应正文(Body)。

    状态行
    HTTP/1.1 200 OK
    响应头信息
    Date: Mon, 10 Jan 2011 05:34:30 GMT
    Server: Apache/2.2.14 (Ubuntu)
    X-Powered-By: PHP/5.3.2-1ubuntu4.2
    Vary: Accept-Encoding
    Content-Length: 20
    Keep-Alive: timeout=15, max=100
    Connection: Keep-Alive
    Content-Type: text/html; charset=UTF-8
    空行 
    响应正文
    <body>
    14:34</body>

    图 3-6 响应消息的构造

  • 状态行

    状态行的内容是请求消息经过服务器处理以后的状态(图 3-7)。

    图 3-7 状态行的构造

    状态码的百位数有特殊含义,代表了响应的几种状态(表 3-1)。常见的状态码有:200(成功)、301 和 302(重定向)、404(找不到资源)、500(服务器内部发生错误)等。

    表 3-1 状态码的说明

    状态码概要
    1xx处理正在继续
    2xx成功
    3xx重定向
    4xx客户端错误
    5xx服务器错误
  • 响应头信息

    响应消息的第 2 行及以后的内容为响应头信息(图 3-6),内容一直到出现空行(只含有换行符的行)为止。以下为典型的响应头信息。

    • Content-Length

      显示响应正文的字节数。

    • Content-Type

      指定为 MIME 类型。HTML 文档的情况下则为 text/html。下表列出了常见的 MIME 类型。

    表 3-2 常见的 MIME 类型

    MIME 类型含义
    text/plain文本
    text/htmlHTML 文档
    application/xmlXML 文档
    text/cssCSS
    mage/gifGIF 图像
    image/jpegJPEG 图像
    image/pngPNG 图像
    application/pdfPDF 文档

    分号之后的 charset=UTF-8 表示 HTTP 响应的字符编码。字符编码必须被正确设置,具体原因及设置方法请参考第 6 章。

  • 如果将 HTTP 比喻为对话

    由于 HTTP 会持续不断进行请求与响应,所以将其比喻为人们的对话或许会更形象。以显示时间的脚本为例,将这个最简单的 HTTP 消息以对话的形式呈现的话,大概就像下面这样 2 。

    顾客:现在几点了?

    店员:15 点 21 分。

    接下来,我们看一个复杂些的 HTTP 消息的例子——“输入-确认-注册”模式的表单。

1 如果 HTTP 协议版本为 1.0,Host 头信息也可以省略。

2 将 HTTP 比喻成对话的灵感,源于书籍《Web 背后的技术》[1] 与《Web 技术入门》[2]。

输入 - 确认 - 注册模式

这里,通过观察“输入-确认-注册”模式中输入表单(Input Form)的 HTTP 消息,希望能够有助于读者更深入地理解 HTTP。

以下分别为输入页面(31-002.php)、确认页面(31-003.php)和注册页面(31-004.php)的代码。

代码清单 /31/31-002.php

<html>
<head><title> 个人信息输入 </title></head>
<body>
<form action="31-003.php" method="POST">
姓名 <input type="text" name="name"><br>
邮箱地址 <input type="text" name="mail"><br>
性别 <input type="radio" name="gender" value=" 女 "> 女
<input type="radio" name="gender" value=" 男 "> 男 <br>
<input type="submit" value=" 确认 ">
</form>
</html>

代码清单 /31/31-003.php

<?php
  $name = @$_POST['name'];
  $mail = @$_POST['mail'];
  $gender = @$_POST['gender'];
?>
<html>
<head><title> 确认 </title></head>
<body>
<form action="31-004.php" method="POST">
姓名 :<?php echo htmlspecialchars($name, ENT_NOQUOTES, 'UTF-8'); ?><br>
邮箱地址 :<?php echo htmlspecialchars($mail, ENT_NOQUOTES, 'UTF-8'); ?><br>
性别 :<?php echo htmlspecialchars($gender, ENT_NOQUOTES, 'UTF-8'); ?><br>
<input type="hidden" name="name" value="<?php echo htmlspecialchars($name, ENT_COMPAT, 'UTF-8'); ?>">
<input type="hidden" name="mail" value="<?php echo htmlspecialchars($mail, ENT_COMPAT, 'UTF-8'); ?>">
<input type="hidden" name="gender" value="<?php echo htmlspecialchars($gender, ENT_COMPAT, 'UTF-8'); ?>">
<input type="submit" value=" 注册  ">
</form>
</html>

代码清单 /31/31-004.php

<?php
  $name = @$_POST['name'];
  $mail = @$_POST['mail'];
  $gender = @$_POST['gender'];
  // 下面开始处理
?>
<html>
<head><title> 注册成功 </title></head>
<body>
姓名 :<?php echo htmlspecialchars($name, ENT_NOQUOTES, 'UTF-8'); ?><br>
邮箱地址 :<?php echo htmlspecialchars($mail, ENT_NOQUOTES, 'UTF-8'); ?><br>
性别 :<?php echo htmlspecialchars($gender, ENT_NOQUOTES, 'UTF-8'); ?><br>
已注册
</body></html>

要在虚拟机上执行的话,可以点击 /31/ 菜单中的“31-002:输入-确认-注册”链接。然后就会显示如下页面(图 3-8)。

图 3-8 输入页面

在页面上填入姓名、邮箱和性别后点击“确认”按钮。这时,HTTP 请求消息就可以在 Fiddler 中看到(图 3-9)。

图 3-9 在输入页面填写完毕后点击“确认”时 Fiddler 中显示的 HTTP 请求消息

  • POST 方法

    此处显示的是从图 3-9 的请求消息中摘取的一些重要内容。

    POST /31/31-003.php HTTP/1.1
    Referer: http://example.jp/31/31-002.php
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 70
    Host: example.jp
                                                                    消息体
    
     ┐
    name=%E5%BE%B3%E4%B8%B8%E6%B5%A9&mail=toku@example.jp&gender=%E7%94%B7┘
    

    请求行开头处的方法变成了 POST。与 GET 不同的是,空行下面所填写的值也被发送了。而这部分内容就被称为消息体(Message Body)。

  • 消息体

    通过 POST 方法发送的请求消息中包含消息体。与响应消息一样,消息头和消息体用空行相隔。要通过 POST 方法发送的值被放在请求的消息体中。

    与 POST 发送值相关的消息头为 Content-Length 和 Content-Type。

    Content-Length 为消息体的字节数。

    Content-Type 为发送值的 MIME 类型,可通过 HTML 的 form 元素设置。默认为 application/x-www-form-unlencoded。这种类型的格式为,“名称 = 值”的组合通过 & 相连,其中,名称和值都经过了百分号编码(Percent-Encoding)。

  • 百分号编码

    中文和特殊符号等不能直接用于 URL,而如果要将它们用在 URL 上的话就需要经过百分号编码。百分号编码是将字符以字节为单位转换成 %xx 的形式。xx 为该字节的十六进制写法。例如,将图 3-8 中输入的文字“德”进行 UTF-8 编码,可得到 E5 BE B3 字节列,百分号编码后即为 %E5%BE%B3。

    根据百分号编码的规则,空格应为 %20,但在 application/x-www-form-unlencoded 的情况下,空格则被特殊处理为 +3 。所以,将“I'm a programmer”进行百分号编码的话,结果就为 I%27m+a+programmer(撇号变成了 %27)。

  • Referer

    请求消息中有时含有 Referer 头信息。它能告诉我们当前请求是从哪个页面链接过来的,值就是那个页面的 URL。除了通过 form 元素发送的请求,a 元素生成的链接或 img 元素的图像等也会产生 Referer 头信息。

    Referer 头信息有时是提升安全性的帮手,有时却能成为问题之源。

    Referer 有益的一面体现在,当我们为了确保安全性而主动检验 Referer 头信息时,通过查看 Referer,能够确认应用程序的跳转是否跟预期一样。但是,同其他头信息一样,Referer 也能由访问者本人通过 Fiddler 之类的工具修改,或者被浏览器插件和其他安全方面的软件修改或删除,所以未必会正确显示链接的来源 4 。

    当 URL 中包含敏感信息时,Referer 就可能会引发安全问题。比如,URL 中包含的会话 ID 通过 Referer 泄漏给外界,从而使自己的身份被他人恶意冒名顶替,就是一个典型的案例。具体情况将在 4.6.3 节详述。

    要点  URL 中包含重要信息时,就有被 Referer 头信息泄密的风险。

  • GET 和 POST 的使用区别

    如何区别使用 GET 方法和 POST 方法呢?

    HTTP 1.1 协议的规范文档 RFC26165 的第 9 章和第 15 章中,记载了区别使用两者的注意点。

    • GET 方法仅用于查阅(获取资源)
    • GET 方法被认为没有副作用
    • 发送敏感数据时应使用 POST 方法

    这里出现了“副作用”这个概念。副作用是指,除了获取资源(内容)以外的其他操作。比如,追加 / 更新 / 删除服务器端的数据、购买商品、注册 / 删除用户等操作。换言之,更新类的页面必须使用 POST 方法。

    另外,GET 方法使用的是 URL 后紧跟查询字符串的形式来传递参数,但由于浏览器和服务器能够处理的 URL 长度是有限的 6 ,所以,当传递的信息量很大时,使用 POST 方法更安全。

    敏感信息应使用 POST 发送,这是因为 GET 方法有下列风险。

    • URL 中指定的参数经由 Referer 泄漏
    • URL 中指定的参数残留在访问日志(Access Log)中

    为解决以上问题,当所发送的请求符合以下任一条件时就应使用 POST 方法,都不符合时才使用 GET 方法。

    • 请求中包含数据更新等副作用时
    • 发送敏感信息时
    • 发送的信息量很多时
  • hidden 参数能够被更改

    继续刚才的输入表单(图 3-8),点击“确认”按钮后浏览器的页面如图 3-10 所示。

    图 3-10 确认画面

    虽然在页面上看不到,但用户在前页面输入的值会以 hidden 参数的形式在 HTML 源代码中记录下来。

    与 FTP 协议或 telnet 协议不同,HTTP 协议无法记忆客户端的当前状态。这种特性被称为 HTTP 的无状态性 7 。因此,状态的记忆需要借助响应(HTML)中的 hidden 参数。

    在页面上点击“注册”按钮后,hidden 参数将被发送给 Web 服务器。此时,在向服务器发送数据之前,我们可以尝试使用 Fiddler 改变 hidden 参数的值。

    首先,在 Fiddler 的 Rules 菜单中,选择“Automatic Breakpoints”-“Before Requests”(图 3-11)。

    图 3-11 在 Rules 菜单中选择“Automatic Breakpoints”-“Before Requests”

    此状态下,点击“注册”按钮后,Fiddler 的界面就变成了图 3-12 所示的情形(选择右侧上方的“WebForms”)。现在,Fiddler 截获到了浏览器的请求消息,并且还未将其传送给服务器。

    图 3-12 Fiddler 接收浏览器的请求消息

    编辑方框中的内容,如图 3-13 所示。

    图 3-13 变更浏览器的请求消息

    接下来,点击“Run to Completion”按钮,变更后的请求就会被发送给服务器。此时,IE 上会显示图 3-14 的页面(虽然显示了“成功注册”的消息,但事实上并没有进行注册处理)。

    图 3-14 浏览器显示了变更后的信息

    以上试验说明,在 HTTP 层面,文本框、单选框的选择项,以及 hidden 参数都被同等对待,在浏览器上无法改变的值(如单选框的选择项和 hidden 参数)也能够被更改。

    要点  浏览器发送的值都能够被变更

    通过实际体验 hidden 参数的变更可以得知,一旦处理 hidden 参数的地方存在安全隐患,就会有被 Fiddler 等代理工具实施篡改和攻击的风险。

  • 将 hidden 参数的更改比作对话

    接下来让我们以对话的形式来再现一下刚才变更 hidden 参数的情形。

    顾客与店员的对话

    顾客:我想要申请会员。

    店员:请提供您的姓名、邮箱地址、性别(男或女)。

    顾客:姓名为德丸浩,邮箱地址为 tokumaru@example.jp,性别为男。

    店员:好的。姓名为德丸浩,邮箱地址为 tokumaru@example.jp,性别为男。请您确认。

    顾客: 不对。姓名为德丸玛利亚,邮箱地址为 maria@example.jp,性别为女。请注册。

    店员:姓名为德丸玛利亚,邮箱地址为 maria@example.jp,性别为女。您的会员身份已注册完毕。

  • hidden 参数的优点

    前面介绍了 hidden 参数的一些隐患,那么 hidden 参数有什么优点呢?虽然 hidden 参数的值能被用户自己改写,但在面对信息泄漏以及被第三方篡改等危险时,hidden 参数却坚不可摧。

    与 hidden 参数形成鲜明对比的是后面将要介绍的 Cookie 和会话(Session)变量。Cookie 和会话变量的缺点是容易招致会话固定攻击。尤其是在尚未登录、并且又使用了地域型域名的情况下,受到 Cookie Monster Bug 的影响,根本就没有有效的办法来防止会话变量的泄漏(参考 4.6.4 节)。

    因此,像认证和授权信息这样需谨防被用户自己更改的信息,应当保存在会话变量中(参考 5.1 节和 5.3 节)。而除此以外的信息,则首先应考虑能否保存在 hidden 参数中。特别是在登录前的状态下,由于不存在与认证、授权相关的信息,因此,原则上要避免使用会话变量,而是应该使用 hidden 参数,从而来防止信息泄漏等。

3 百分号编码属于 URL(URI)的规范,application/x-www-form-unlencoded 属于 HTML 的规范,所以两者存在细微差别。

4 关于使用 Fiddler 来改变参数,后面讲 hidden 参数时会进行详述。

5 http://tools.ietf.org/html/rfc2616

6 虽然 RFC2616、RFC1738 及 RFC3986 中并未规定 URL 的长度上限,但各个浏览器都存在上限值。

7 与此相反,像 FTP 和 telnet 这种能够记忆当前状态的特性,叫作有状态性。

无状态的 HTTP 认证

HTTP 支持认证功能。HTTP 认证根据实现方式可细分为 Basic 认证、NTLM 认证和 Digest 认证等。正如 HTTP 是无状态的协议一样,HTTP 认证同样也是无状态的。

下面让我们看一下 HTTP 认证中最简单的 Basic 认证。

  • 体验 Basic 认证

    Basic 认证的概要如图 3-15 所示。Basic 认证下,当浏览器请求一个需要认证的网页时,服务器会先向浏览器返回“401 Unauthorized(未认证)”状态码。浏览器收到此状态码后,会显示要求输入 ID 和密码的画面,然后再将输入的 ID 和密码添加到请求信息中,再次向服务器发送。

    图 3-15 Basic 认证的概要

    Basic 认证大多通过设置 Web 服务器来实现,而也能通过 PHP 来编写代码。以下为使用 PHP 的 Basic 认证的例子。

    代码清单 /31/31-010.php

    <?php
      $user = @$_SERVER['PHP_AUTH_USER'];
      $pass = @$_SERVER['PHP_AUTH_PW'];
     
      if (! $user || ! $pass) {
        header('HTTP/1.1 401 Unauthorized');
        header('WWW-Authenticate: Basic realm="Basic Authentication Sample"');
        echo " 请输入用户名和密码 ";
        exit;
      }
    ?>
    <body>
    已通过认证 <br>
    用户名 :<?php echo htmlspecialchars($user, ENT_NOQUOTES, 'UTF-8'); ?>
    <br>
    密码 :<?php echo htmlspecialchars($pass, ENT_NOQUOTES, 'UTF-8'); ?> <br></body>
    
    

    以上代码仅用于试验,所以 ID 和密码输入任意值都能通过认证,而 ID 或密码任意一方为空白就会认证失败。认证失败时,按照 Basic 认证的规定会输出以下头信息。

    HTTP/1.1 401 Unauthorized
    WWW-Authenticate: Basic realm="Basic Authentication Sample"
    
    

    如果要在虚拟机上运行,可以点击 /31/ 菜单的“31-010:Basic 认证试验”。第一次请求时浏览器没有发送 ID 和密码,所以 31-010.php 返回了 401 状态码。这时,HTTP 信息的截图如图 3-16 所示。浏览器收到 401 状态码后,就会显示要求输入 Basic 认证的 ID 和密码的对话框(图 3-17)。

    图 3-16 返回 401 状态码的 HTTP 消息

    图 3-17 Basic 认证的 ID 和密码输入对话框

    这次我们来尝试一下输入 ID“user1”和密码“pass1”,输入完毕后点击 OK 按钮,HTTP 请求消息再次被发送。这次会附带以下的 Authorization 头信息。

    Authorization: Basic dXNlcjE6cGFzczE=
    
    

    Basic 后面的字符串内容,是 ID 和密码以冒号相隔组成的字符串、再经过 Base64 编码后的结果。可以使用 Fiddler 的 Encoder 功能进行解码确认。在 Fiddler 的 Tools 菜单中选择“Text Encode/Decode”,就会显示出 TextWizard 对话框,然后将 dXNlcjE6cFGzczE= 复制进去,点击对话框左边的“From Base64”单选按钮(图 3-18),就能在画面中央的文本框中看到“user1:pass1”字符串。

    图 3-18 使用 Fiddler 附带的 TextWizard 来解码 Base64

    而这时,浏览器上显示的就是下图所示的画面。可以看出,PHP 脚本成功读取了 Basic 认证的 ID 和密码。

    图 3-19 认证成功

    Basic 认证成功一次以后,再向 http://example.jp/31/ 下面的目录发送请求时,浏览器就会自动附带 Authorization 消息头。因此,认证对话框只在最初的时候显示一次,看上去认证状态似乎被记住了,但实际上每次请求时都会发送 ID 和密码,认证状态并没有被保存在任何地方。换言之,Basic 认证也是无状态的。而正是因为 Basic 认证的无状态性,所以也就不存在注销(Logout)的概念。

    Basic 认证可以被比喻为银行业务柜台的对话。

    顾客:请帮我查一下账户余额。

    柜员:请提供您的银行卡号和密码。

    顾客:请帮我查一下账户余额。卡号为 12345,密码为 9876。

    柜员:余额为 5 万元。

    顾客:请向卡号 23456 转账 3 万元。卡号为 12345,密码为 9876。

    柜员:转账完毕。

    顾客和柜员之间的交流是无状态的。无关上下文,顾客每一次都要提供所有必要的信息。因此,就算一开始就进行转账也能正常处理。

    专栏:认证与授权

    至此我们未经特别说明就一直使用着“认证”(Authentication)这个术语。认证是指,通过一些方法手段来确认操作者确实是其本人。Web 应用常见的认证方法除了 Basic 认证,还有通过 HTML 表单使用户填写 ID 和密码的表单认证,以及使用 SSL 客户端证书的客户端认证等。

    与认证相对的术语是“授权”(Authorization)。授权是指,授予已经通过认证的用户一些权限。具体表现为,让用户能够对数据进行阅览 / 更新 / 删除、在线转账、在线购物等。

    由于页面上并不会特意区分认证与授权,所以用户很容易将两者混为一谈。Web 应用的普遍流程为,在用户输入 ID 和密码通过认证以后,立刻就会被授予一些权限。但是,在开发应用及考虑安全性时,最好能明确认证与授权这两者的区别,并养成区别使用的习惯。

    关于认证和授权,5.1 节和 5.3 节会分别进行详述。

Cookie 与会话管理

前面我们已经提到,由于 HTTP 协议的无状态性,服务器端不能保存客户端的状态。但是,在应用程序中,保持客户端的状态却是相当常见的需求。

比如,在线购物网站中的“购物车”就是一个典型的案例。购物车记住了用户在哪些商品上点击了“购买”按钮。

另外,记住用户登录后的认证状态也是一种常见的需求。虽然使用 HTTP 认证就能使浏览器记住 ID 和密码,但不使用 HTTP 认证时,记忆认证状态的任务就落在了服务器身上。而像这种记忆应用程序状态的功能就叫作“会话管理”。

为了实现会话管理,HTTP 引入了名为 Cookie 的机制。Cookie 相当于服务器下达给浏览器的命令,让其记住发送给它的“名称 = 变量”这种格式的值。由于 Cookie 会被用于实现会话管理,因此,下面就让我们结合 PHP 中的会话管理来对其进行说明。

下面的示例应用是用户认证和用户信息显示的简化版。由以下 3 个页面构成:ID 和密码输入页面(31-020.php)、ID 和密码认证页面(31-021.php)、个人信息(ID)显示页面(31-022.php)。在虚拟机上执行时,可在 /31/ 菜单中点击“31-020: 使用 Cookie 的会话管理”。

代码清单 /31/31-020.php

<?php
  session_start();  // 会话开始
?>
<html>
<head><title> 请登录 </title></head>
<body>
<form action="31-021.php" method="POST">
用户名 <input type="text" name="ID"><br>
密码 <input type="password" name="PWD"><br>
<input type="submit" value=" 登录 ">
</form>
</body>
</html>

代码清单 /31/31-021.php

<?php
  session_start();  // 会话开始
  $id = @$_POST['ID'];
  $pwd = @$_POST['PWD'];
  // 用户名和密码中任意一项未输入时则登录失败
  if ($id == '' || $pwd == '') {
    die(' 登录失败 ');
  }
  $_SESSION['ID'] = $id;
?>
<html>
<head><title> 登录 </title></head>
<body>
登录成功
<a href="31-022.php"> 我的账号 </a>
</body>
</html>

代码清单 /31/31-022.php

<?php
  session_start();  // 会话开始
  $id = $_SESSION['ID'];
  if ($id == '') {
    die(' 请登录 ');
  }
?>
<html>
<head><title> 我的账号 </title></head>
<body>
用户名 :<?php echo htmlspecialchars($id, ENT_NOQUOTES, 'UTF-8'); ?>
</body>
</html>

同 Basic 认证的例子一样,随便输入 ID 和密码就能成功通过认证。页面跳转流程如下图所示。

图 3-20 示例应用的页面跳转

最初显示 31-020.php 的 ID 和密码输入页面时,返回的响应消息如下(仅列出要点)。

HTTP/1.1 200 OK
Set-Cookie: PHPSESSID=gg5144avrhmdiaelvh80l4lb53; path=/


Content-Length: 279
Content-Type: text/html; charset=UTF-8

<html>
<head><title> 请登录 </title></head>
<body>【以下略】

通过 Set-Cookie 响应头信息,Web 服务器向浏览器下达了记住此 Cookie 值的指示。

在登录页面上输入 ID 和密码后点击“登录”按钮,浏览器就会向服务器发送如下请求。

POST /31/31-021.php HTTP/1.1
Referer: http://example.jp/31/31-020.php
Content-Type: application/x-www-form-urlencoded
Host: example.jp
Content-Length: 18
Cookie: PHPSESSID=gg5144avrhmdiaelvh80l4lb53



ID=user1&PWD=pass1

记住了 Cookie 值的浏览器,从此再向相同网站(example.jp)发送请求时,就会同时发送此 Cookie 值(PHPSESSID=…)。Cookie 可以设置有效期限,没有设置有效期限的 Cookie 会在浏览器被关闭之前一直有效。

Cookie 中 PHPSESSID 的值被称为会话 ID,它是访问会话信息的关键。31-021.php 中,认证成功后的用户 ID 被保存在会话变量 $_SESSION['ID'] 中。随后程序在 31-022.php 中取得了此用户 ID。保存在会话变量中的信息,在会话失效之前随时都能被访问。

  • 使用 Cookie 的会话管理

    Cookie 能让浏览器记忆少量数据,但保存应用程序的数据时几乎不会使用 Cookie,理由如下。

    • Cookie 能保存的值的数量和字符串长度有限
    • Cookie 的值能被用户自己看到或更改,所以不适用于存储敏感信息

    因此,可以采用在 Cookie 中保存类似于“受理编号”的会话 ID,实际对应的值则保存在服务器端的方法。这种方法被称为“使用 Cookie 的会话管理”,目前有着非常广泛的应用。PHP 等主流 Web 应用开发工具中已经提供了这种会话管理的机制。

  • 会话管理的拟人化解说

    将解说 Basic 认证时使用的银行窗口业务的比喻,转换为会话管理版本,其详情如下。

    顾客:你好。

    柜员:您的受理编号为 005。请提供您的银行卡号和密码。

    顾客:受理编号为 005。卡号为 12345,密码为 9876,请确认。

    柜员:身份核实完毕。

    顾客:受理编号为 005。请帮我查下余额。

    柜员:余额为 5 万元。

    顾客:受理编号为 005。请向卡号 23456 的账户转账 3 万元。

    柜员:转账完毕。

    对话中的受理编号即为会话 ID。顾客每次都要向柜员汇报受理编号,正如浏览器每次都自动向服务器发送 Cookie 一样。

    但是,005 这个受理编号多少让人感到不安。因为随便换一个相近的号码,就有可能假冒他人。比如像下面这样。

    恶人:你好。

    柜员:您的受理编号为 006。请提供您的银行卡号和密码。

    恶人将受理编号减去 1 变成了 005。假设受理编号为 005 的顾客已经通过了身份核实。

    恶人:受理编号为 005。请向卡号 99999 的账户转账 3 万元。

    柜员:转账完毕。

    仅仅更改了受理编号,就能成功使用他人的账户进行转账。

    由此可见,会话 ID 不能使用连续的数字,而应当使用位数足够长的随机数,刚才看到的 PHPSESSID 的值有 26 位也正是出于这个原因。综上,会话 ID 需要满足如下需求。

    需求 1:会话 ID 不能被第三方推测

    需求 2:会话 ID 不能被第三方劫持

    需求 3:会话 ID 不能向第三方泄漏

    需求 1 的会话 ID 不能被推测,本质上要求的是随机数的质量问题。如果随机数存在规律性,就能够通过收集足够多的会话 ID 来推测别人的会话 ID。因此,会话 ID 的随机数可以使用密码学级别的伪随机数生成器生成,伪随机数生成器的范例在电子政府推荐密码列表 8 中有所记载。

    但是实际开发的过程中,会话 ID 并非自己手动生成,而应该使用 Web 应用开发工具(PHP、Tomcat、.NET 等)提供的会话 ID。这些主流开发工具经受着全球研究者的调查,因此,即使生成会话 ID 的处理有问题,也一定会被作为安全隐患而得到改善。以笔者多年诊断安全隐患的经验来看,自己实现的会话管理机制中混入安全隐患的例子不在少数。所以务必不要自己来实现会话管理机制。关于会话管理不完善所导致的安全隐患问题,4.6 节中会进行细述。

    要点  使用开发工具提供的会话管理机制。

    接着,关于需求 2 的会话 ID 不能被劫持,最初使用银行窗口业务的比喻来说明的流程中其实存在安全问题。具体请看下面对话中的攻击流程。

    恶人:你好。

    柜员:您的受理编号为 9466ir8fgmmk1gn6raeo7ne71。请提供您的银行卡号和密码。

    恶人暂时离开柜台并等待来客。有顾客进入银行时,恶人冒充银行柜员向顾客搭话。

    恶人:您的受理编号为 9466ir8fgmmk1gn6raeo7ne71。

    顾客:知道了。

    顾客走向柜台。

    顾客:我的受理编号为 9466ir8fgmmk1gn6raeo7ne71。

    柜员:请提供您的银行卡号和密码。

    顾客:受理编号为 9466ir8fgmmk1gn6raeo7ne71。卡号为 12345,密码为 9876,请确认。

    柜员:身份核实完毕。

    顾客执行完身份确认后恶人也走向了柜台。

    恶人:受理编号为 9466ir8fgmmk1gn6raeo7ne71。请向卡号 99999 的账户转账 3 万元。

    柜员:转账完毕。

    像这样,恶人(攻击者)劫持正规用户的会话 ID 来进行攻击的手法被称为会话固定攻击(Session Fixation Attack)。详情将在 4.6.4 节中讲述,这里我们可以先尝试修复此安全隐患。从客人进入银行开始。

    有顾客进入银行时,恶人冒充银行柜员向顾客搭话。

    恶人:您的受理编号为 9466ir8fgmmk1gn6raeo7ne71。

    顾客:知道了。

    顾客走向柜台。

    顾客:我的受理编号为 9466ir8fgmmk1gn6raeo7ne71。

    柜员:请提供您的银行卡号和密码。

    顾客:受理编号为 9466ir8fgmmk1gn6raeo7ne71。卡号为 12345,密码为 9876,请确认。

    柜员:身份核实完毕。您新的受理编号为 eut1j15a058pm8gapa87l937h6。

    顾客执行完本人身份确认后恶人也走向了柜台。

    恶人:受理编号为 9466ir8fgmmk1gn6raeo7ne71。请向卡号 99999 的账户转账 3 万元。

    柜员:您还没有进行身份核实。请提供您的银行卡号和密码。

    由于顾客通过认证时受理编号(会话 ID)发生了变化,所以攻击者试图使用原来的受理编号进行转账时就遭到了“您还没有进行身份核实”的提示。通过这个方法,可有效防止会话固定攻击。

    要点  认证后改变会话 ID。

    下面让我们继续看一下需求 3 的防止会话 ID 泄漏。

  • 会话 ID 泄漏的原因

    会话 ID 一旦遭到泄漏就有可能发生伪装事件,所以必须采取防范措施。会话 ID 泄漏主要有以下几种原因。

    • 发行 Cookie 时的属性指定有问题(稍后讲述)
    • 会话 ID 在网络上被监听(参考 7.3 节)
    • 通过跨站脚本漏洞等应用中的安全隐患被泄漏(参考第 4 章)
    • 由于 PHP 或浏览器等平台存在安全隐患而被泄漏
    • 会话 ID 保存在 URL 中的情况下,会通过 Referer 消息头泄漏(参考 4.6.3 节)

    网络传输线路上若存在监听装置,会话 ID 就有被窃取的风险。虽然从外部无法得知具体哪里会有监听装置,但在公共无线网等理论上易于监听的环境中,会话 ID 被盗的风险是非常高的。

    为了保护会话 ID 不被监听,采用 SSL(Secure Socket Layer)加密是行之有效的方法,但发行 Cookie 时也需要注意属性的指定。

  • Cookie 的属性

    生成 Cookie 时可以设置很多属性。先前看到的 PHPSESSID 在生成时指定了“path=/”,这就是一个属性。

    生成 Cookie 时的主要属性如表 3-3 所示。

    表 3-3 Cookie 的属性

    属性含义
    DomainCookie 发送对象服务器的域名
    PathCookie 发送对象 URL 的路径
    ExpiresCookie 的有效期限。未指定则表示至浏览器关闭为止
    Secure仅在 SSL 加密的情况下发送 Cookie
    HttpOnly指定了此属性的 Cookie 不能被 JavaScript 访问

    其中,涉及安全性的 3 个重要属性为 Domain、Secure、HttpOnly。

    • Cookie 的 Domain 属性

      Cookie 在默认情况下只能被发送到与其绑定的服务器。虽然从安全性方面考虑这样是最安全的,但有时也需要能向多个服务器发送的 Cookie,这时就要用到 Domain 属性。

      图 3-21 展示了指定 Domain 属性后的 Cookie 被发送给服务器的情况。由于指定了 Domain=example.jp,因此,Cookie 就被发送给了 a.example.jp 和 b.example.jp,而 a.example.com 则因为域名不同而没有发送。

      图 3-21 指定 Cookie 的 Domain 属性

      假如 a.example.jp 的服务器在 Set-Cookie 时指定了 Domain=example.com,此 Cookie 就会被浏览器忽略。这是因为如果可以在 Cookie 中指定不同域名,就可能发生前述的会话固定攻击,所以 Cookie 是不能指定不同域名的。

      未指定 Domain 属性时,Cookie 只被发送至生成它的服务器。换言之,未指定 Domain 属性的 Cookie 发送范围最小,最安全。而设置 Domain 属性时稍有疏忽,就会产生安全隐患。

      举例来说,假设 example.com 是服务器租赁商,foo.example.com 和 bar.example.com 都是托管在此租赁服务器上的网站。如果 foo.example.com 网站发送的 Cookie 中指定了 Domain=example.com,此 Cookie 就会被泄漏至 bar.example.com。

      由此可见,不设置 Cookie 的 Domain 属性是最佳实践。

      要点  原则上不设置 Cookie 的 Domain 属性

       

      专栏:Cookie Monster Bug

      笔者所在公司的网站域名是 hash-c.co.jp,生成的 Cookie 中指定的域名最短也应当为 hash-c.co.jp。但是,使用一些旧版本浏览器时 Cookie 的域名却被指定成了 .co.jp。这一问题就被称为“Cookie Monster Bug”。

      使用存在 Cookie Monster Bug 的浏览器会极易遭受会话固定攻击。因为域名为 .co.jp 的 Cookie 也能匹配 amazon.co.jp 和 yahoo.co.jp 等其他 .co.jp 的域名,这就意味着能够对这些网站任意指定 Cookie。

      在 Internet Explorer 8(IE8)中使用地域型域名时也存在 Cookie Monster Bug。举例来说,笔者所住的横滨市的域名为 city.yokohama.jp,而横滨市内的地方政府或企业、团体、个人等也都能够获得以 yokohama.jp 结尾的域名。也就是说笔者能够申请获得 tokumaru.kanazawa.yokohama.jp 这个域名(kanazawa 为横滨市金沢区)。问题是,使用 Internet Explorer 时,网站 tokumaru.kanazawa.yokohama.jp 能够生成域名为 yokohama.jp 的 Cookie。

      地域型域名在地方政府的网站中有着广泛的应用,却容易遭到会话固定攻击。最近, .lg.jp 作为地方政府的域名开始被使用,横滨市也启用了 city.yokohama.lg.jp 域名。因此,建议使用地域型域名的网站,在加强防范会话固定攻击的同时,也不妨考虑一下迁移至其他形式的域名。

    • Cookie 的安全属性

      设置了 Secure 属性(下述为安全属性)的 Cookie 仅在 SSL 传输的情况下能够被发送给服务器。而未设置安全属性的 Cookie 则无关是否为 SSL 传输,都会被发送。

      指定 Cookie 的安全属性是为了确保 Cookie 在 SSL 的情况下发送。详情请参考 4.8.2 节。

    • Cookie 的 HttpOnly 属性

      设置了 HttpOnly 属性后,JavaScript 就不能访问该 Cookie 了。

      恶意使用 JavaScript 进行跨站脚本攻击从而取得 Cookie 信息,是窃取 Cookie 中会话 ID 的典型案例。而 Cookie 中设置了 HttpOnly 属性后,就能防止 JavaScript 窃取 Cookie 信息。

      后面专门讲述跨站脚本时也会提到,其实设置了 HttpOnly 属性也无法彻底抵御跨站脚本攻击,但是能加大攻击的难度。而设置 HttpOnly 属性通常不会带来坏处,所以应当时常给 Cookie 加上 HttpOnly 属性。

      使用 PHP 的情况下,给 Cookie 添加 HttpOnly 属性,只要在 php.ini 中添加如下设置即可。

      session.cookie_httponly = on
      
      

      关于 Cookie 的 HttpOnly 属性,在讲跨站脚本漏洞的防范对策时还会再次提到。

8 http://www.cryptrec.go.jp/list.html

总结

为了有助于理解 Web 应用的安全隐患,本节讲述了 HTTP、Basic 认证、Cookie、会话管理的相关知识。当前大多数应用都采用 Cookie 来进行会话管理,这在认证结果的保存等安全性方面扮演着重要角色。

作为本节的应用篇,下节将讲述被动攻击和同源策略。

参考文献

[1] 山本阳平 .(2010).《Web を支える技術-HTTP、URI、HTML、そして REST》(《Web 背后的技术-HTTP、URI、HTML 和 REST》). 技術評論社 .

[2] 小森裕介 .(2010).《「プロになるための Web 技術入門」——なぜ、あなたは Web システムを開発できないのか》(《“Web 技术入门”——为什么你无法开发 Web 系统》). 技術評論社 .

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文