文章
| Oracle + PHP
部署 PHP 系列第 2 部分:为 PHP 应用程序增加数据安全性 如何在不牺牲数据安全性的情况下创建并部署轻型 PHP 前端应用程序。
使用 PHP 构建基于 Web 的、数据库驱动的应用程序,是一种使可靠用户便于访问公司数据的日益普遍方法。 但是,PHP 开发人员必须认识到,将敏感且通常是任务关键的数据与可由公司网络外部访问的应用程序相连将存在严重的安全风险。 在麦吉尔大学的校友事务部,我们选择将 Oracle 的工业级安全性 (Industrial-Strength Security) 与 PHP 的易开发性相结合来确保数据的可用性,而不会造成对整个数据库安全性的负面影响。 在部署 PHP 系列的这一部分中,我将突出强调创建和部署这种解决方案的主要步骤,并向您演示一个示例应用程序的代码。 数据访问授权:前端与后端的对比 您在此的目标是创建一个轻型前端应用程序,这个应用程序将允许具有不同安全权限的用户无缝地获取相应数量数据的访问权。 同时,您需要确保不违反数据访问策略 — 即使突破 PHP 前端的安全性。 因此,在前端应用程序中实施数据访问授权是不适当的,这是因为任何设法绕过应用程序安全机制的人都能无限制地访问数据库中的所有数据。 这当中不仅包括故意突破应用程序安全性的人,还包括可以运行 ad hoc 查询或有权访问报告工具(如 Infomaker 或 Crystal Reports)的合法用户。 为了更好地理解这一问题,请看图 1 中所示的两种情况:
在第一种情况中,应用程序对用户进行身份验证,并确定用户的权限。 在这种情况下,数据库不是身份验证过程的一部分,而只是为任何提供有效凭证的应用程序提供数据。 但是,在第二种情况里,数据库 — 而不是应用程序 — 根据用户的身份确定为其提供多少数据。 在此,应用程序只对用户进行身份验证,并将这一信息传递给强制执行安全策略的数据库服务器,并为每个用户返回为其量身定做的记录集。 因此,为了尽量减小不必要的数据暴露风险,应将安全策略直接附加在数据上 — 而不是通过前端应用程序实施。 幸好,在 Oracle8i 及更高版本中,Oracle 数据库提供了正需要的一组特性:Oracle 虚拟专用数据库 (VPD) 和安全应用程序环境,提供细粒度访问控制。 VPD 使开发人员能够控制对单个数据行的访问,并确保两个安全权限不同的用户绝不会收到相同的数据集 — 即便在他们直接访问完全相同的表或视图时也是如此。 应用程序环境允许创建可用于唯一标识每个用户会话的自定义会话变量。 本文所讨论的应用程序依赖 Oracle VPD 和应用程序环境来确保数据库服务器负责执行系统的安全要求,并确保无论以何种方式访问数据,每个用户收到的刚好都是安全策略能为其提供的行数。 部署策略 在讨论应用程序要求及实施细节之前,先看一下最重要的系统要求。 为构建该示例系统,需要具有:
系统的物理体系结构可能会有所不同,且可以简单到只是一个承载 Web 服务器、PHP 引擎以及 Oracle 数据库的服务器。 由于 PHP 前端应用程序仅仅旨在将输出显示给最终用户,而大部分安全问题都由数据库处理,因此对 Web 服务器的操作系统、Web 服务器软件及 PHP 版本的要求不是很苛刻。 本文所讨论的示例应用程序建立在以下配置之上: Web 服务器
本文的第二部分介绍了一家假象的企业 XYZ Corporation,它正在构建一个安全的基于 Web 的前端应用程序来连接其中央数据库。 这里讨论此应用程序的功能和安全要求;勾勒其逻辑体系结构;引导完成实现过程(包括 PHP 与 Oracle 部分);并讨论两个部分如何在一个单一安全模型下共同运行。 问题概述 XYZ Corporation 是一家向其他公司出售计算机软件和硬件的中型企业。 公司使用 Oracle 数据库来对记录其日常运营,特别是维护销售记录。 假定 XYZ 的销售人员很分散。 为了对销售活动有准确的了解,公司决定创建一个基于 Web 的销售报表,它将列出迄今发生的所有交易。 依照内部安全策略,需要根据用户的部门和职位过滤销售记录。 总的安全要求可归结为如下:
为满足这些要求,XYZ 决定实现一个 VPD 安全解决方案,以确保无论用户如何访问数据都会执行上述策略。 在此情况下,数据授权是由数据库执行的,这有助于降低前端应用程序的复杂性,并提高了系统的整体安全性。 由于前端承担的任务现在实际上减少为呈现输出和简单的会话管理,因此 XYZ 决定采用易于学习的轻型脚本语言 PHP 而非全面的框架(如 J2EE 或 .NET)。 解决方案概述 图 2 中展示了所建议系统的高级逻辑体系结构:
此系统包括两个主要部分: PHP Web 应用程序和 Oracle 后端。 当用户最初试图访问系统时,应用程序验证其是否已登录(有一个有效的用户会话),并在需要的时候提示输入凭证。 如果凭证有效,后端将此用户的唯一 ID 号返回给 Login 脚本,该脚本接着启动一个新的用户会话,并将该用户的 ID 作为一个会话变量附加到此会话。 此时,这个用户可以访问销售报表 — 通过连接到数据库、创建连接环境,并执行 SELECT SQL 查询来检索数据。 数据访问授权由附加在 Sales Transactions 表上的安全策略执行。 通过生成自动附加到原始 SQL 语句的谓词( WHERE 子句),此策略动态修改原始查询。 此谓词的确切形式取决于在环境中指定的用户身份。 其结果是,尽管所有应用程序用户都执行完全相同的 SELECT 查询并使用相同的数据库连接参数,但每个用户收到的行集都是唯一的。 实现 Oracle 后端功能。 此应用程序依赖于图 3 中所示的数据结构。
这个数据结构由 4 个表组成。 EMPLOYEE 表和 DEPARTMENT 表表示 XYZ 的组织结构,表之间的关系显示出每个员工隶属于一个部门,每个部门有一个经理。 销售交易列表位于 TRANSACTION 表中,这就是需要安全策略保护的表。 最后,应用程序用户(即 PHP 前端应用程序的用户)列表出现在 LOGIN 表中 — 它由 EMPLOYEE 表中的员工列表得来。示例代码下载中的清单 1 包含用于设置表和插入数据的代码。 这些表是以 SALES 模式创建的。 实现此系统的最重要的步骤之一是设置 VPD 安全策略,它将保护 TRANSACTION 表中的数据。 这将在接下来的部分中讨论。 设置 VPD。 为我们的示例系统设置一个 VPD 解决方案包括以下步骤:
使用 DBMS_RLS 程序包的功能创建安全策略。 为确保一般数据库用户不能擅自改动安全策略,您将创建一个专用用户帐户 (SEC_MANAGER) 并授予其权限来执行 DBMS_RLS 中的函数和过程: GRANT EXECUTE ON DBMS_RLS TO SEC_MANAGER; SEC_MANAGER 还需要有设置应用程序环境的能力: GRANT CREATE ANY CONTEXT TO SEC_MANAGER;自此,您将依赖使用 SEC_MANAGER 用户帐户来设置其余的结构。
由于前端 PHP 应用程序的所有用户都会重用 Oracle 用户帐户 (SALES) 访问数据库,因此您将不得不依赖 Oracle 的应用程序环境来跟踪用户的身份。 要做到这一点,您需要创建环境空间 (TEST_SECURITY): CREATE CONTEXT TEST_SECURITY USING sec_manager.pkg_sec_public 并创建一个分配会话专用环境变量的过程。 最后,您需要为 SALES 用户帐户授予执行此过程的权限。 (查看清单 2 中的代码,这些代码用于在 PKG_SEC_PUBLIC 程序包内创建 SET_CONTEXT 过程,并向 SALES 用户授予在此包中执行该过程的权限 [ GRANT EXECUTE ON PKG_SEC_PUBLIC TO SALES ]。) 为获得更好的可管理性,也可以通过角色授予权限。 例如,如果某些时候应用程序要求一个以上的帐户,您可以创建一个 EMPLOYEE_ROLE,并将这个角色分配给任意新数据库用户帐户。 下一步是创建策略函数,用以执行在安全要求中列出的规则。 在我们的例子中,策略函数背后的逻辑非常简单:
策略函数位于 PKG_SEC_POLICY 程序包中,且对所有用户都可用 ( GRANT EXECUTE ON PKG_SEC_POLICY TO SALES)。 (策略函数 ( FN_SELECT ) 的完整代码可以在示例代码下载中的清单 3 中找到。) 为简化代码,您还要创建一个辅助函数,它判断一个给定用户是否为经理,并返回此人的部门 ID。该辅助函数 ( FN_MANAGER ) 的代码位于清单 4 中, SALES 模式的内容如图 4 所示。 请注意,上面的情况只是实现给定安全要求的多种方法之一。 策略函数的结构和逻辑是完全由用户定义的,可以按需要做到尽可能地简单或复杂。
最后,为了能够动态过滤销售交易记录,您必须将 FN_SELECT 函数附加到 TRANSACTION 表来创建安全策略。 这需要调用 DBMS_RLS 程序包的 ADD_POLICY 过程:
BEGIN
dbms_rls.add_policy( 'sec_manager', 'transaction', 'transaction_select',
'sec_manager', 'pkg_sec_policy.fn_select', 'select');)
END;
不可避免的是,查询安全策略保护的表将需要在数据库部分做一些额外的工作。 为 WHERE 子句中引用的列添加一个索引可以改善这种状况。
现在,通过附加由 FN_SELECT 函数返回的谓词,此策略已设置为动态修改所有进入的涉及到 TRANSACTION 表的 SELECT SQL 查询。 重要的是,安全策略并不限于 SELECT 语句,也可以用于监视 INSERT、UPDATE 和 DELETE 查询。 还可以将同一个策略用于一个以上的表、将多个策略应用于同一个表,或多个策略重用一个策略函数。 除了表以外,这些策略还可以附加在视图上。 实现 PHP 前端功能。 PHP 前端应用程序用于验证用户、呈现输出以及提供简单的会话管理。 可以使用以下一组脚本来实现:
后缀 inc 表明应将此脚本包含在其他脚本内,并从其他脚本执行,而非从自己执行。 这并不是 PHP 要求,而只是一个不成文的惯例,用以帮助区分不同的脚本。 但是,请注意,在实际情况中,您可能希望将“includes”放置在一个 Web 用户不能直接访问的特殊目录(如 cgi-bin)中。 应用程序如何运行。 现在,我们来仔细看一下每个脚本以及它们是如何协同运行的。 图 5 显示了正常的应用程序流:
要查看销售交易报表,用户必须调用 transaction_list.php — 它首先执行 authorize.inc.php 脚本以确保用户已登录且已通过系统身份验证:
require("authorize.inc.php");
authorize.inc.php 的工作是检查用户的会话是否包含一个名为“SESSION_TOKEN”的会话变量,并验证这一变量的内容是否有效。 为了简单起见,此脚本不执行除验证变量长度是否大于零之外的任何特殊检查。 强烈建议在将其用于任何验证例程之前过滤“SESSION_TOKEN”变量的内容。 如果此变量缺失或未初始化,脚本会将把用户重定向到登录窗体:
if (!strlen($_SESSION["SESSION_TOKEN"]) > 0) {
header("Location: login_form.php?return_url=".$strReturnURL."&error_msg=".urlencode($strErrorMessage));
exit
}
登录脚本由两部分组成: 用于收集用户凭证的窗体 ( login_form.php ) 以及验证用户输入并依照 LOGIN 表检查凭证的内含脚本 ( login.inc.php )。 尽管不要求有一个独立的内含脚本,且很容易在窗体中包含相同的功能,但如果您想要将这个脚本与其他窗体重用,则该方法的灵活性稍大一些。
除了确保均已提供登录名和密码外,登录脚本不提供对用户输入的详尽验证。 不过,再一次强烈建议对用户的输入进行严格过滤,以使应用程序免受跨站脚本攻击 (cross-site scripting) 或 SQL 注入式攻击。 一旦输入通过验证,脚本就调用 connect.inc.php 的 GetConnection() 函数,打开一个数据库连接,并通过执行下列查询尝试获取用户的 ID:
SELECT emp_id,login FROM LOGIN WHERE login='login_name' AND password='password'; 如果上述查询没有返回结果,则登录脚本向用户显示错误消息“Invalid user name or password”,并提示输入新的凭证。 否则,脚本启动一个新的 PHP 会话,并将从 LOGIN 表接收的用户 ID 指定为“SESSION_TOKEN”变量的内容。 用户 ID 是初始化附加在 TRANSACTION 表上的 VPD 策略的关键部分;因此,将这一信息作为一个明文会话变量是 不 适用于真实系统的。 有很多种方法可以保护“SESSION_TOKEN”变量的内容并使会话劫持 (session hijacking) 的风险最小。 这些方法可能包括加密、哈希运算或添加更多的参数,如截止时间戳记、加密 salt,甚至是将令牌有效载荷绑定到用户 IP 地址的某个部分。 不过,本文不讨论这些方法。 关于应用程序如何处理会话最后要注意的是,它要求用户在其浏览器中启用 cookie,不过,您可以通过在 GET 或 POST 请求上携带会话 ID 来避免这一点。 呈现数据。 最后,在登录成功且 authorize.inc.php 让用户通过后,就由 transaction_list.php 脚本来控制了。 transaction_list.php 首先所做的就是打开一个新的数据库连接: $rsConnection = GetConnection($_SESSION["SESSION_TOKEN"]);要确保这一次(与在登录阶段进行连接不同)连接函数获取保存在“SESSION_TOKEN”变量中的用户 ID(不过再次强调要保持警觉,在将其传递给函数前要过滤会话令牌的内容)。 然后,用户 ID 被提供给 SET_CONTEXT 过程,此过程在数据库服务器中为给定连接(即 Oracle 会话)初始化安全应用程序环境。
一旦创建了连接,脚本就执行此 SQL 查询: SELECT T_ID,T_AMOUNT,EMP_NAME,EMP_POSITION,DEP_NAME FROM TRANSACTION, EMPLOYEE, DEPARTMENT WHERE TRANSACTION.EMP_ID = EMPLOYEE.EMP_ID AND EMPLOYEE.DEP_ID = DEPARTMENT.DEP_ID;如果调用成功,脚本就使用 PHP 的 Oracle 调用接口 (OCI) 函数 OCIFetchInto 遍历结果记录集并在一个 HTML 表格中呈现每个行。 为了简单起见,这个脚本使用一个显式 SQL 语句检索数据。不过,对真实系统来说,使用存储过程更为恰当。
VPD 与 PHP 组合的好处 分析这个示例系统的代码,很容易认识到用以 VPD 为中心的方法创建安全的 Web 应用程序的优势。 如您所见, transaction_list.php 脚本没有区分不同用户的代码,且实际上,对任何通过登录脚本的用户执行完全相同的 SQL 语句。 与之相似, connection.inc.php 脚本使用相同的 Oracle 帐户来打开数据库连接。 不过,虽然前端应用程序知道用户的身份(“SESSION_TOKEN”变量中的用户 ID),但它仅仅将其传递给数据库服务器,从而使数据库服务器(而不是 PHP 应用程序)来负责执行附加在 TRANSACTION 表上的安全策略。 如果您试图不提供用户身份进行连接会怎样呢? 应用程序仍将创建一个有效的 Oracle 连接,但是在这种情况下,此连接不附加有任何环境,这使安全策略将所有数据隐藏在 TRANSACTION 表中。 也就是说,对于任何这样的连接(通过 PHP 前端或直接到数据库), TRANSACTION 表会显示没有任何数据。 总的来说,将数据授权工作从前端应用程序延伸到数据库服务器有以下好处:
试用 现在,一旦应用程序的后端和前端都已就绪,您就可以实施若干测试用例来确保系统功能符合安全要求。 TRANSACTION 表当前包含下列行:
下面表中列出了 5 个用例以及每个用例的预期行为和实际结果。
结论 本文中所讨论的示例应用程序旨在演示将 PHP 和 Oracle 相结合来实施一个访问受 VPD 保护数据的易于开发的前端的概念和好处。 请注意,示例系统为适应文章的内容做了简化。 尽管有功能很全,但它缺乏企业实用应用程序的某些特性,如对输入的过滤和验证、对 PHP 会话数据的充分保护机制、加密等等。 不过,这个应用程可以作为创建真实系统(可以提供访问机密且任务关键数据的轻型但安全的接口)的一个好起点。 Mikhail Seliverstov [mikhail.seliverstov@mcgill.ca] 是位于蒙特利尔的麦吉尔大学校友会的 Web 编程小组的领导人。 他专攻构建基于 Web 的大型系统的安全性方面的问题。 在过去 3 年中,Seliverstov 作为首席应用程序架构师和开发人员,帮助领导了一个大型的 PHP/Oracle 开发项目。 将您的意见发送给我们 |