Chapter 6. Security

The main reason for setting up security in your PHP/Oracle application is to protect it against unauthorized access or alteration of the data.

In the preceding chapter, you learned how to build an authentication system in PHP using the Auth class from the PEAR library. You also saw several examples of how this class might be extended to suit the needs of a particular application. However, it is important to realize that implementing an authentication system is only the first step in building a secure application. Once a user is successfully authenticated, your application should determine whether that user is authorized to access the requested database resources, thus defining different levels of permissions for different users. It is always a good idea to implement authorization within the database as it guarantees that no one will be able to bypass the application's security, even if the connecting to the database is made directly.

This chapter discusses how to effectively use the security features of both PHP and Oracle together, examining the fundamental aspects of building a secure PHP/Oracle application. In particular, you will learn how to:

  • Separate security management and data

  • Implement an authentication/authorization system

  • Hash sensitive data

  • Implement fine-grained access using database views

  • Mask column values returned to the application

  • Use Oracle Virtual Private Database (VPD) technology

Securing PHP/Oracle Applications

In this section, you will learn how to build a simple secure PHP/Oracle application. The intent of this example is to demonstrate how to use simple security mechanisms available in PHP and Oracle in a complementary way in order to protect sensitive data from unauthorized disclosure and prevent data from unauthorized modification or destruction of data.

In particular, you will see how to organize data structures within the database to ensure a secure access to the application data stored in the database, and how to build secure PHP scripts through which application users will access that data.

An important point to note here is that the example discussed in this section uses only those Oracle database features that are available in any of Oracle the database editions, including Oracle Database Express Edition.

Another point to note about the example discussed here is that, in order to follow it, you do not necessarily need to have the samples from the previous chapters built. You simply will need to borrow the testAuth.php script described in the PEAR::Auth in Action section in Chapter 5 and the usr.acoounts table created as described in the An Example of When to Use a Stored Subprogram section in Chapter 3, before proceeding to the sample.

Authenticating Users

In the Using Standard Classes section in Chapter 5, you saw an example of an authentication system built using the Auth package from the PEAR library. In particular, you saw how the PEAR::Auth class can be used to build secure pages in your PHP/Oracle application, preventing unauthenticated users from accessing secure information. The following steps outline how it works:

  • Users cannot access any secure application page until they log in. Users log in through a login form, supplying a valid username/password pair maintained in the database.

  • Once logged in, the user gains access to secure pages. He or she doesn't need to supply his or her credentials again when moving from one secure page to another. Before a secure page is displayed, a check of whether a session with valid authentication information exists takes place.

  • An authenticated user is automatically logged out when the session holding the authentication information expires or the specified amount of idle time has passed, whichever is soonest.

    Note

    In a real-world application, in addition to this functionality, you would, no doubt, implement the logout functionality based on the logout method of the PEAR::Auth class, providing a way for users to log out.

For simplicity, the example discussed in Chapter 5, on its secure page, displays a hello message including the name of the authenticated user and some text applicable to all application users. Regardless of the credentials being used by the end users to get authenticated, the application connects all users to the single database schema. The following diagram offers a high-level view of that interaction.

Authenticating Users

As you can see from the figure, you don't need to create a database account for each application user just to make it possible for him or her to access the data stored in the database—all application users are associated with the same single database account.

Being commonly used in database-driven web applications, this all-to-one type of mapping will be used in all the examples discussed throughout this chapter. The next sections examine how the authentication system discussed here can be improved for better security.

Separating Security Management and Data

In the example discussed in the Using Standard Classes section in Chapter 5, the application uses the same database schema to connect to the database and to hold the database tables queried from the application. However, from a security standpoint, using the same single database schema in this case is definitely not the best way to go.

The fact is that when connecting to a database schema, the user obtains full access to all the database objects belonging to that schema. In practice, this means that after a connection to the database is established the application can not only issue SELECT statements against the database objects it works with, but also issue DML (INSERT, UPDATE, DELETE) and DDL (DROP TABLE, for example) statements against those objects, thus creating a potential security issue. Such issues can be resolved by using database schemas as discussed in the following sections.

Using Two Database Schemas to Improve Security

To improve security, you might create another database schema to be used only for establishing connections, and then give only SELECT privileges on the database objects that your application queries to that schema.

As an example of where this technique might be useful, let's turn back to the example discussed in the PEAR::Auth in Action section in the preceding chapter. Looking through the testAuth.php script shown in the listing, you may notice that it authenticates users against the accounts table, which in turn is created as described in the An Example of When to Use a Stored Subprogram section in Chapter 3. Since the testAuth.php script connects to the database through the same database schema where the accounts table resides, this represents a weak spot in the application's defense.

Before you proceed with this example, make sure that you have the accounts table installed in your database. To do this, issue the following SELECT statement from SQL*Plus when connected as usr/usr:

SELECT * FROM accounts;

If you have performed the SQL statements shown in the An Example of When to Use a Stored Subprogram section in Chapter 3, you should receive results that might look as follows:

USR_ID FULL_NAME PSWD NUM_LOGONS
---------- -------------------- ---------- ----------
bob Bob Robinson pswd 32

Note

The num_logons column was added to the accounts table when performing SQL statements shown in the Controlling Transactions from PHP section in Chapter 4. For the example discussed in this section, it does not matter if your accounts table contains the num_logons column or not.

If you still don't have the accounts table, you need to create it now, using the appropriate statements from the An Example of When to Use a Stored Subprogram section in Chapter 3.

Once you have that set up, you can create a new database schema through which the testAuth.php script will connect to the database. Then, you can grant the SELECT privileges on the accounts table to the newly created schema. The following SQL statements are required to perform these tasks.

CONN /AS sysdba

Using this SQL statement, you create a new database schema that will be used by the application to connect to the database.

CREATE USER app_conn IDENTIFIED BY appconn;

Once a database schema is created, you should grant all the required privileges to it. When granting privileges, you should always remember the least privilege principle, which implies that users should be given only the minimum privileges required to do their job and nothing else.

Note

To make managing and controlling privileges easier, Oracle uses roles, combining related privileges into groups. In addition to predefined roles available in Oracle, you can create and then employ your own roles, grouping privileges as needed.

In this example, you grant only the default CONNECT role to the newly created schema, thus allowing users accessing only this account to connect to the database:

GRANT CONNECT TO app_conn;

Note

Starting with Oracle Database 10g Release 2, the CONNECT role has only the CREATE SESSION privilege—the minimum required to establish a connection to the database. Other privileges, such as CREATE TABLE and CREATE VIEW, were removed from the CONNECT role for security reasons.

Next, you grant the SELECT privilege on the usr.accounts table to the app_conn schema, so that the application users using app_conn schema to connect to the database will be able to view data stored in this table, but not alter it.

GRANT SELECT ON usr.accounts TO app_conn;

Now that you have the app_conn schema set up and granted the CONNECT role, you can use this schema instead of usr in your application in order to connect to the database. So, you should revoke the CONNECT role from the usr schema for security reasons.

REVOKE CONNECT FROM usr;

Once you have performed all the SQL statements discussed above, you need to slightly modify the testAuth.php script discussed in the PEAR::Auth in Action section in Chapter 5 to use the newly created app_conn database schema instead of usr.

In fact, all you need to modify is the array of options passed to the storage container of the Auth object, as shown below:

$auth_opts = array(
'dsn'=>'oci8://app_conn:appconn@localhost:1521/orcl',
'table'=>'usr.accounts',
'usernamecol'=>'usr_id',
'passwordcol'=>'pswd',
'db_fields' => '*',
'cryptType'=>'none'
);

Once you've done that, you can then test the updated authentication system. To do this, you might run the appPage.php script discussed in the Securing Pages with PEAR::Auth section in the preceding chapter. When testing the sample, you should note that while the above changes improve the security of the authentication scheme used here, from the user's point of view, the sample behaves as before.

Using Three Database Schemas to Improve Security

Having two database schemas—one for establishing connections and the other for holding database resources used by the application—is undoubtedly a good idea from the point of view of security. This way the schema used for establishing a connection to the database has no rights to modify the data accessed by the application. However, it is still not an optimal security solution—everyone who can obtain the database connection credentials embedded in the application source code will be able to issue a SELECT query against the usr.accounts table, thus obtaining access to all application users' accounts stored in this table.

One possible solution to this problem is to create another database schema that will serve as an intermediate point of connection between the above two schemas. In this schema, you might create a set of stored procedures providing secure access to database resources. Graphically, it might look like the following figure.

Using Three Database Schemas to Improve Security

Using the security solution shown in the figure, you restrict access to database data by allowing users to manipulate it only through the stored procedures that in turn have a restricted set of privileges on the data they manipulate. Residing in a separate database schema, these stored procedures are used to safely access resources stored in another schema. For better security, the schema used for establishing connections has only the rights to execute these stored procedures but not to create or alter them.

You need to execute the following SQL statements to create a new database schema for holding stored procedures that will be used to safely access database data.

CONN /AS sysdba

Next, you create the sec_adm database schema that will be used to perform the security related tasks:

CREATE USER sec_adm IDENTIFIED BY secadm;

You grant the RESOURCE role to the newly created schema so that you can create database resources in it. Note that you should not grant the CONNECT role to this schema because your application will use only the app_conn schema to connect to the database:

GRANT RESOURCE TO sec_adm;

To be able to view the usr.accounts table from sec_adm, you grant the SELECT privilege on this table to sec_adm:

GRANT SELECT ON usr.accounts TO sec_adm;

Since the SELECT privilege on usr.accounts table granted to the app_conn schema is no longer needed, you should revoke it.

REVOKE SELECT ON usr.accounts FROM app_conn;

Employing PL/SQL Packages and Table Functions to Securely Access Database Data

After you have created a single database schema for performing security-related tasks, the next step is to create appropriate database objects in that schema. In this example, you might create a single stored function that will take a user name as the parameter and then return the corresponding row from the usr.accounts table if any.

The following listing shows how to create the sec_pkg PL/SQL package containing the f_auth function that will be used to securely access the usr.accounts table.

CONN /AS sysdba
CREATE OR REPLACE PACKAGE sec_adm.sec_pkg IS
TYPE acc_rec_typ IS RECORD (
USR_ID VARCHAR2(10),
FULL_NAME VARCHAR2(20),
PSWD VARCHAR2(10),
NUM_LOGONS NUMBER(38));
TYPE acc_rec_set IS TABLE OF acc_rec_typ;
FUNCTION f_auth(usrid VARCHAR2) RETURN acc_rec_set PIPELINED;
END sec_pkg;
/
CREATE OR REPLACE PACKAGE BODY sec_adm.sec_pkg IS
FUNCTION f_auth(usrid VARCHAR2)
RETURN acc_rec_set PIPELINED IS
acc_rec acc_rec_typ;
BEGIN
SELECT * INTO acc_rec FROM usr.accounts WHERE usr_id=usrid;
PIPE ROW(acc_rec);
RETURN;
END;
END sec_pkg;
/

Now let's discuss the statements in the above listing, step by step.

As you might recall from the preceding section, the sec_adm database schema is granted only the RESOURCE role. Therefore, you cannot connect to the sec_adm database schema directly, because it lacks the CREATE SESSION privilege required for this operation. In this example, you connect to the database as sysdba and then create the desired database objects in sec_adm.

With the help of the CREATE PACKAGE statement shown in the above listing, you create a new PL/SQL package specification, declaring types, variables, and subprograms that can be referenced from both inside the package and outside it. Note that in this example you create a package specification from outside the database schema to which that package will belong. That is why in this case, the parameter of the CREATE PACKAGE statement must contain that schema name followed by the package name.

Note

In the Performing Authorization Based on the User Identity section later in this chapter, you will see how package variables can be used to hold authentication information during a database session.

In the package specification, you first define the acc_rec_typ user-defined record type, which should reflect the structure of a usr.accounts table row. To see the exact structure of the usr.accounts table, you might issue the following SQL statement from SQL*Plus when connected as sysdba:

DESC usr.accounts

If you have performed the SQL statements described in the An Example of When to Use a Stored Subprogram section in Chapter 3, as well as the statements in the Controlling Transactions from PHP section in Chapter 4, then the above statement should produce the following results:

Name		 Null?		 Type
----------------------------------------- -------- ------------
USR_ID		 NOT NULL		 VARCHAR2(10)
FULL_NAME					 VARCHAR2(20)
PSWD		 NOT NULL		 VARCHAR2(10)
NUM_LOGONS 					 NUMBER(38)

If you did not perform the SQL statements from the Controlling Transactions from PHP section in Chapter 4, you will not see num_logons field in the above output. If so, you must not include this field in the acc_rec_typ record type either.

Next, you declare the acc_rec_set table type of acc_rec_typ. You will use this type as the return type of the f_auth package function declared in the next line.

The f_auth function will be called from outside the package. Note the use of the PIPELINED keyword when declaring the f_auth function. By declaring a function as PIPELINED, you specify that this function will return a collection of rows like a database table or view when queried.

Note

Pipelined functions, also known as pipelined table functions, may come in very handy when you need to protect sensitive database data from unauthorized access. Unlike regular database tables and views that may be queried with SELECT statements containing no WHERE clause, and, thus, returning all the rows from the queried object, a table function can be organized so that it never returns all the rows from the table or view queried inside the function. For more information on pipelined functions, refer to Oracle documentation: Tuning PL/SQL Applications for Performance chapter in the Oracle Database PL/SQL User's Guide and Reference book.

Next, you use the CREATE PACKAGE BODY statement to define the sec_pkg package body, in which you define the code for the f_auth function declared in the package specification.

You begin creating the f_auth function by declaring its header, as defined earlier in the package specification. In this example, the f_auth function takes only one parameter, namely usrid, and then returns the corresponding row from the usr.accounts table if the SELECT statement executed within this function finds one.

Then, you define the acc_rec variable of the acc_rec_typ type declared earlier in the package specification. This variable will be used to hold the row retrieved by the SELECT statement.

Next, you query the usr.accounts table to obtain the row whose usr_id field value is equal to the value of the parameter passed to the f_auth function. It is important to note that since the usr_id column in the usr.accounts table is the primary key column, the SELECT statement used here cannot return more than one row. Otherwise, you would have to process the retrieved rows individually in a loop.

Now, look at the following statement. With it, you might create the sec_pkg.f_auth function so that it could theoretically return more than one row to the caller:

CREATE OR REPLACE PACKAGE BODY sec_adm.sec_pkg IS
FUNCTION f_auth(usrid VARCHAR2)
RETURN acc_rec_set PIPELINED IS
BEGIN
FOR acc_rec IN (SELECT * FROM usr.accounts WHERE usr_id=usrid)
LOOP
PIPE ROW(acc_rec);
END LOOP;

RETURN;
END;
END sec_pkg;
/

In this particular example, the sec_pkg.f_auth function will always return either one row or none, regardless of whether you perform the SELECT operation in a loop or not. This is because a given username cannot be associated with more than one row in the usr.accounts table.

You use the PIPE ROW statement to pipeline the row retrieved out of the f_auth function to the caller. Once an appropriate row has been pipelined, you use the RETURN statement to end the execution of the function and return control to the caller.

Using the %ROWTYPE Attribute

As an alternative to using a user-defined record when defining the acc_rec_typ type in the sec_pkg package discussed in the preceding example, you might define that type as usr.accounts%ROWTYPE, which could save you the trouble of needing to know whether the usr.accounts table contains the num_logons column.

The problem with this approach in this example is that you're using the %ROWTYPE attribute with the accounts table that resides in the usr database schema, while the sec_pkg PL/SQL package, which employs %ROWTYPE, resides in a different schema, namely sec_adm. Looking through the %ROWTYPE Attribute section in the PL/SQL User's Guide and Reference Oracle documentation book, you may notice that the syntax of the %ROWTYPE attribute does not imply specifying a database schema name in front of the table (or view) name used with the attribute.

Note

Actually, you can still do so. However, there is no guarantee that this will work as expected. In this particular example, using usr.accounts%ROWTYPE instead of the user-defined record may result in a runtime error when the sec_pkg.f_auth function is invoked.

One way to solve this problem is to define a view on the usr.accounts table in the sec_adm schema, and then use the name of that view with %ROWTYPE to declare a record type representing a row in the usr.accounts table. From a security perspective, it is important that the newly created view, while having the same structure as the usr.accounts base table, selects no rows from this table. This can be achieved by specifying, say, 1=0 in the WHERE clause of the view's defining query.

The following listing shows how to create a view on the usr.accounts table in the sec_adm schema, and then rewrite the sec_pkg package created as described in the preceding section so that it uses the name of the newly created view with %ROWTYPE when declaring a record type representing a row in the usr.accounts table.

CONN /AS sysdba
CREATE VIEW sec_adm.accounts_empty_v AS
SELECT * FROM usr.accounts WHERE 1=0;
CREATE OR REPLACE PACKAGE sec_adm.sec_pkg IS
TYPE acc_rec_set IS TABLE OF accounts_empty_v%ROWTYPE ;
FUNCTION f_auth(usrid VARCHAR2) RETURN acc_rec_set PIPELINED;
END sec_pkg;
/
CREATE OR REPLACE PACKAGE BODY sec_adm.sec_pkg IS
FUNCTION f_auth(usrid VARCHAR2)
RETURN acc_rec_set PIPELINED IS
acc_rec accounts_empty_v%ROWTYPE;
BEGIN
SELECT * INTO acc_rec FROM usr.accounts WHERE usr_id=usrid;
PIPE ROW(acc_rec);
RETURN;
END;
security, PHP/Oracle applications%ROWTYPE attribute, usingEND sec_pkg;
/

By issuing the CREATE VIEW statement in the above listing, you create an empty view whose structure is the same as the structure of the usr.accounts table. It is important to understand that the only goal of this view is to provide information about the usr.accounts table's structure, rather than providing information stored in the table. So, you specify 1=0 in the WHERE clause of the view's query to make sure that it returns no rows from the base table and, thus, the view is empty.

Another good thing, from a security standpoint, is that you don't have to grant any privileges on the accounts_empty_v view to the app_conn schema through which your application establishes connections to the database. This is because you reference the accounts_empty_v view within the sec_pkg package, which belongs to the same database schema as the view.

In the package specification, you declare a table type of accounts_empty_v%ROWTYPE. Note the use of the accounts_empty_v view that was created earlier in this listing. The table type declared here will be used as the return type of the f_auth package function.

Next, you declare the acc_rec variable of accounts_empty_v%ROWTYPE. You will then use this variable to hold the account information retrieved from the usr.accounts table.

Then you define the SELECT statement that queries the usr.accounts table, inserting appropriate account data into the acc_rec variable.

Now that you have examined the sec_pkg package's code and learned how login credentials are processed within the f_auth function defined in the package, you might want to look at how the example will use it in the authentication process.

However, before you start using the f_auth function defined in the sec_pkg package, you need to grant the EXECUTE privilege on the sec_pkg package to the app_conn database schema so that the f_auth function can be executed from within the applications connecting to the database through that schema. This can be done with the following SQL statement:

CONN /AS sysdba
GRANT EXECUTE ON sec_adm.sec_pkg TO app_conn;

Note

It is important to understand that the EXECUTE privilege on a package allows the schema that is granted it only to execute subprograms from the package, not to alter or drop it.

Once you've done all that, you can turn back to the testAuth.php script, making it work with the sec_adm.f_auth function rather than with the usr.accounts table directly.

One problem here is that you cannot simply specify sec_adm.f_auth instead of usr.accounts as the value for the table parameter in the array of options passed to the storage container of the Auth object. This is because the sec_adm.f_function takes the parameter, which is passed to the function when the user submits the login form.

Building a Custom Storage Container for the PEAR::Auth Class

A simple, yet effective solution to the above problem is to create a custom storage container for the Auth class that would work with a table function rather than with a regular database table or view.

In the spirit of code reuse, you don't have to write a storage container from scratch. Instead, you might take advantage of an existing one, customizing it as needed. The Auth PEAR package ships with a number of storage containers, which are implemented as classes and stored in single files in the /Auth/Container directory located within the PEAR directory. It is important to note that the /Auth/Container directory is hard-coded into the Auth class. Therefore, if you want to create a custom storage container, you have to store it in this directory.

As you may recall from Chapter 5 Object-Oriented Approach, PHP allows you to extend existing classes by adding new methods and properties and overriding the existing ones. In this example, you need to extend the class that implements the DB container used by PEAR::Auth, so that the new class can work with a table function that takes a user ID as the parameter, rather than working with a regular table or view.

Examining the files in the /Auth/Container directory, you may find that the DB container is implemented as the Auth_Container_DB class and resides in the Auth/Container/DB.php file. Now, to build a custom class upon Auth_Container_DB, create a new file, say, DB_func.php, in the Auth/Container directory and insert the following code

<?php
//File: DB_func.php
require_once 'Auth/Container/DB.php';
class Auth_Container_DB_func extends Auth_Container_DB
{
function fetchData($username, $password,
$isChallengeResponse=false){
$this->options['table']="TABLE(".
$this->options['table']."('".$username."'))";

return parent::fetchData($username, $password,
$isChallengeResponse=false);
}
}
?>

In the above script, you include the Auth/Container/DB.php file because it contains the Auth_Container_DB class that acts as the parent class for the Auth_Container_DB_func class defined in this script.

Next, you begin the class definition, specifying that the newly created class is built upon Auth_Container_DB.

The only parent class method you override in the Auth_Container_DB_func class is the fetchData method. This method is invoked immediately after a user submits the login form, using the given username to fetch the corresponding authentication information from the database.

Within the fetchData method, you override the table parameter in the array of options passed to the container. In the new value of the table parameter, you include the old value of the parameter, which in turn includes the name of the sec_adm.f_auth function, and specify the username variable as the function's input argument, which in turn is an input parameter of the fetchData method.

Once the table parameter is set up to the new value, you call the fetchData method of the parent class.

Testing the Authentication System

Now it's time to put the newly created authentication system into action. Note that the testAuth.php script shown below, which was originally discussed in the PEAR::Auth in Action section in Chapter 5, is modified to support user authentication against the sec_adm.sec_pkg.f_auth table function rather than the usr.accounts table.

<?php
//File: testAuth.php
require_once "Auth.php";
$auth_opts = array(
'dsn'=>'oci8://app_conn:appconn@localhost:1521/orcl',
'table'=>"sec_adm.sec_pkg.f_auth",
'usernamecol'=>'usr_id',
'passwordcol'=>'pswd',
'db_fields' => '*',
'cryptType'=>'none'
);
$auth_opts['usernamecol'] = strtoupper($auth_opts['usernamecol']);
$auth_opts['passwordcol'] = strtoupper($auth_opts['passwordcol']);
$a = new Auth('DB_func', $auth_opts);
$a->setExpire(5);
$a->start();
if ($a->getAuth()) {
print '<font face="Arial">';
print "<h2>Hello, ".$a->getAuthData('FULL_NAME')."!</h2><br/>";
print '</font>';
} else {
exit;
}
?>

In the above script, you specify a table function as the value of the table parameter in the array of options passed to the storage container in the same way you would specify a regular table. In this example, to reference the f_auth function, you use dot notation, specifying the database schema and then the package to which this function belongs.

By specifying DB_func as the first parameter of the Auth's constructor, you tell the Auth object to use the DB_func storage container discussed in the preceding section.

After you have updated the testAuth.php script as shown above, you can run the appPage.php script discussed in the Securing Pages with PEAR::Auth section in Chapter 5 to test the authentication system developed in this section. As in the example from Chapter 5, you may use the bob/pswd username/password combination to get authenticated.

After successful authentication, you will see the secure page produced by the appPage.php script. You may reload this page several times by clicking the here link on it until the authentication session expires (for testing purposes, it is set to 5 seconds in this example). When this happens, you are redirected to the login page again.

As you may conclude, from the end-user point of view, the updated example discussed here behaves exactly like the sample discussed in Chapter 5. What this means in practice is that you can apply security techniques discussed in this section not only when developing a new application but also when improving the security of an existing application.

Performing Authorization Based on the User Identity

Now that you have an authentication system installed and working, it's time to think about authorization. So far, all application users in the example discussed here are associated with the same database schema and, once successfully authenticated, have the same privileges to access the data stored in the database. Going one step further, you might implement an authorization schema so that a given user has access only to certain database resources.

Graphically, it would look like the following figure:.

Performing Authorization Based on the User Identity

The diagram in the figure shows authentication in action. Once a user is authenticated, he or she is authorized to access only his or her own record in the table holding authentication information. In a real-world situation, you probably will want to authorize the user to access not only his or her account record but also the related records in another table or tables.

One way to achieve this functionality is to store information about an authenticated user in the database session and then use this information when performing authorization checks. It is interesting to note that a database session is created when a user connects to the database and ends when the user disconnects (explicitly or implicitly).

Using Sessions to Hold Information about the Authenticated User

As you may recall from Chapter 4, if you're establishing a connection to the database from PHP with the oci_connect function, then the connection is automatically closed when the script ends. The fact is that the DB storage container, which is used by the PEAR::Auth object in the sample discussed here, uses the oci_connect function in the long run to connect to the database. What this means is that once the script that performs authentication ends, the database connection used by that script and, thus, the corresponding database session are closed as well.

On the other hand, the _authsession PHP session, which is set up by the PEAR::Auth object to hold the authentication information once a user is successfully authenticated, exists until it expires or the specified amount of idle time has passed.

To handle this problem, you have to transmit the information about the current authenticated user to the database before performing any database-related operations from within PHP. In particular, you have to set appropriate database session variables to the values held in the corresponding variables of the _authsession PHP session, prior to issuing any queries against the database within a script.

Note

The same technique is used when implementing row-level security with Virtual Private Database (VPD). You will see this in action in the Using VPD to Implement Row-Level Security section later in this chapter.

Holding a User's Information in Package Variables

The easiest way to pass a user's information to the database is to use PL/SQL packages. The fact is that the state of a package persists for the lifetime of a database session (unless you marked that package SERIALLY_REUSABLE). Moreover, the state of a package is private to each database session, which makes it possible for you to use package variables for keeping information about the current user.

Later in this section we look at how to create two PL/SQL packages that will be used to transmit and hold information about the current authenticated user in the database session. The idea is to have one package to hold the user information and the other to manipulate the variables from the first package, setting them to appropriate values and getting them when necessary.

Note

The fact is that the EXECUTE privilege granted on a package to a database schema permits the users connected to the database through that schema to execute any public subprograms in the package and access any public package variables. You cannot grant EXECUTE privilege for a certain subprogram or variable in a package.

Thus, this separation of the packages allows for better security. This is because you have to grant the EXECUTE privilege only on the package containing the 'setter' and 'getter' methods to the schema used for establishing connections to the database, while the package holding user information in its package variables will be inaccessible from that schema. This ensures that the only way for users to pass authentication information to the database is to use the 'setter' package procedures.

You start by creating the sec_adm.app_cxt_pkg package as follows:

CONN /as sysdba
CREATE OR REPLACE PACKAGE sec_adm.app_cxt_pkg IS
userid VARCHAR2(40);
END;
/

In this simple case, you define the sec_adm.app_cxt_pkg package containing only one variable, userid, which will hold the name of the application user being connected. Since you are not going to grant any privileges on the sec_adm.app_cxt_pkg package to any other database schema, the userid package variable can be referenced only within database objects belonging to the sec_adm schema.

Next, you create the sec_adm.set_cxt_pkg package as follows:

CREATE OR REPLACE PACKAGE sec_adm.set_cxt_pkg IS
FUNCTION get_userid RETURN VARCHAR2;
PROCEDURE set_userid(usrid VARCHAR2);
END;
/

In the above package, you declare the 'getter' function and the 'setter' procedure, which are then defined in the package body. Note that the function takes no parameter, while the procedure takes only one parameter, namely userid.

Then you create the package body for the sec_adm.set_cxt_pkg package:

CREATE OR REPLACE PACKAGE BODY sec_adm.set_cxt_pkg IS
FUNCTION get_userid RETURN VARCHAR2
AS
BEGIN
RETURN app_cxt_pkg.userid;
END;
PROCEDURE set_userid(usrid VARCHAR2)
AS
BEGIN
IF (SYS_CONTEXT('USERENV', 'IP_ADDRESS') = '127.0.0.1')
THEN
app_cxt_pkg.userid:=usrid;
END IF;
END;
END;
/

The get_userid package function used here simply returns the value of the app_cxt_pkg.userid package variable.

In this example, the app_cxt_pkg.userid package variable is set to the value passed to the set_cxt_pkg.set_userid procedure as the parameter only if the web/PHP server through which the user is being connected to the database resides on the same machine as the database server. To obtain the IP address of the web/PHP server, you use the SYS_CONTEXT function with the built-in USERENV namespace as the first parameter and IP_ADDRESS as the second parameter, which is associated with the USERENV namespace.

Note

If your web/PHP server and database server reside on different machines, you have to specify the actual IP address of the web/PHP server here, rather than specifying 127.0.0.1.

Performing this additional security check for callers of the set_cxt_pkg.set_userid procedure allows you to make sure that only users connected through a certain web/PHP server pass authorization.

Note

Checking the IP address at this stage is definitely a good idea. However, in a real-world situation, you should not rely on a single check&mdash;after all, IP addresses can be spoofed. It is good practice to check a combination of things when making access-control decisions. For example, you might check whether the request occurs during normal operating hours, thus ensuring timely access to the database resources.

Finally, you grant the EXECUTE privilege on the sec_adm.set_cxt_pkg package to the app_conn schema, thus making it possible for applications connecting to the database through this schema to call the set_userid procedure and the get_userid function.

GRANT EXECUTE ON sec_adm.set_cxt_pkg TO app_conn;

Once you have created packages to hold and manipulate information about the current authenticated user, the next step is to set up database objects that will use these packages to provide secure access to protected resources.

Protecting Resources Based on Information about the Authenticated User

To be in line with the least privilege principle, which implies that each user should be given only the minimum privileges required to do his or her job and nothing else, you might implement an authorization system so that each user can see only the set of records related to his or her account record in the usr.account table. In the simplest case, you might authorize an authenticated user to access only the record representing his or her account.

Using the following statements, you create a view on a table function that will take the sec_adm.set_cxt_pkg.get_userid getter function as the parameter and retrieve the corresponding row from the usr.accounts table.

CONN /as sysdba
CREATE OR REPLACE VIEW sec_adm.accounts_v AS
SELECT usr_id, full_name
FROM TABLE(sec_adm.sec_pkg.f_auth(sec_adm.set_cxt_pkg.get_userid));
GRANT SELECT ON sec_adm.accounts_v TO app_conn;

You don't have to include all the columns from the base table when creating a view. In this example, you exclude the pswd column from the select list of the view's defining query.

As you can see, the sec_adm.accounts_v view discussed here is based on a table function, rather than a regular table. Specifically, it is based on the sec_adm.sec_pkg.f_auth table function discussed in the preceding section, which returns a row from the usr.accounts table based on the username passed in as the parameter.

Since the sec_adm.accounts_v view is supposed to contain the usr.accounts table's row representing the current authenticated user, it would be a good idea to insert a couple of new records into the usr.accounts table, before you proceed to testing the view.

You could insert rows into usr.accounts as shown below:

INSERT INTO usr.accounts (usr_id, full_name, pswd) VALUES
('maya', 'Maya Silver', 'mayapwd'),
INSERT INTO usr.accounts (usr_id, full_name, pswd) VALUES
('john', 'John Stevenson', 'johnpwd'),
COMMIT;

With that done, you can turn back to the testAuth.php script discussed in the Testing the Authentication System section earlier in this chapter. The testAuthor.php script shown below is a version of testAuth.php revised to use authorization based on the name of the current authenticated user.

<?php
//File: testAuthor.php
require_once "Auth.php";
require_once "DB.php";
$auth_opts = array(
'dsn'=>'oci8://app_conn:appconn@localhost:1521/orcl',
'table'=>"sec_adm.sec_pkg.f_auth",
'usernamecol'=>'usr_id',
'passwordcol'=>'pswd',
'db_fields' => '*',
'cryptType'=>'none'
);
$dbh = DB::connect($auth_opts['dsn']);
if(DB::isError($dbh)) {
die($dbh->getMessage());
}
$auth_opts['dsn'] = $dbh;
$auth_opts['usernamecol'] = strtoupper($auth_opts['usernamecol']);
$auth_opts['passwordcol'] = strtoupper($auth_opts['passwordcol']);
$a = new Auth('DB_func', $auth_opts);
$a->setExpire(5);
$a->start();
if ($a->getAuth()) {
$username= $a->getUsername();
$rslt =$dbh->query("BEGIN sec_adm.set_cxt_pkg.set_userid('".$username."'), END;");
if (PEAR::isError($rslt)) {
print $rslt->getMessage();
}
print '<font face="Arial">';
print "<h2>Hello, ".$a->getAuthData('FULL_NAME')."!</h2><br/>";
print '</font>';
} else {
exit;
}
?>

In the above script, you establish a connection to the database using the connect method of the PEAR::DB class, passing the DSN string defined in the array of options to be passed to the storage container used by the Auth object.

In the DSN string used here, you specify an existing instance of PEAR::DB with the dsn parameter instead of a DSN string.

In this example, you use the getUsername method of the Auth object to obtain the name of the current authenticated user.

Next, you set the sec_adm.set_cxt_pkg.userid package variable to the name of the authenticated user obtained by the getUsername method.

It is interesting to note that the script doesn't stop its execution on failure to set the sec_adm.set_cxt_pkg.userid package variable. This makes sense here, since having the sec_adm.set_cxt_pkg.userid package variable undefined simply means that the sec_adm.accounts_v view defined at the beginning of this section contains no record, and, thus, the user will see no account information on the secure page shown in the following figure:

Protecting Resources Based on Information about the Authenticated User

This secure page displays account information of the current authenticated user. Information is displayed only if the user has been successfully authenticated and the sec_adm.set_cxt_pkg.userid package variable has been successfully set to the user's account name.

To see the page shown in the figure, you should run the appPageInfo.php script shown next, and then enter maya/mayapwd as the username/password combination on the login page.

<?php
//File: appPageInfo.php
require_once "testAuthor.php";
$thisPage='"'."appPageInfo.php".'"';
$rslt =$dbh->query('SELECT * FROM sec_adm.accounts_v'),
if (PEAR::isError($rslt)) {
print $rslt->getMessage();
}
print '<font face="Arial">';
print "<h3>Here's your account information</h3>";
$row = $rslt->fetchRow(DB_FETCHMODE_ASSOC);
print '<table border="1" cellpadding="5">';
print '<tr>';
print '<td><b>Account name</b></td><td>'.$row['USR_ID'].'</td>';
print '</tr>';
print '<tr>';
print '<td><b>Full name</b></td><td>'.$row['FULL_NAME'].'</td >';
print '</tr>';
print '</table>';
print '<br/>';
print "Click <a href=".$thisPage.">here</a> to reload this page";
print '</font>';
?>

Let's walk through exactly what the above script does.

Including testAuthor.php at the beginning of the appPageInfo.php script guarantees that only an authenticated user will see the page produced by appPageInfo.php.

Note the use of the $dbh variable representing the PEAR::DB instance created in the testAuthor.php script discussed earlier in this section. So, you don't need to create another instance of PEAR::DB because testAutor.php has been included in the script.

Then, you fetch the results of the query issued earlier. In this example, you specify DB_FETCHMODE_ASSOC as the fetch mode, which tells the fetchRow method of PEAR::DB to return results as an associative array.

For testing purposes, you include here the link to the same page. In a real-world application, it would be a link to another secure page of the application.

Hashing

It is critical that sensitive data is securely sent over the network. For example, it is always a good idea to transmit user passwords over the network in a secure manner, rather than transmitting them in clear text. One way to achieve this is by using hashing.

Note

Hashing is the process of converting a plaintext string of variable length to a fixed-length string, a hash value, which serves as a digital "fingerprint" of the input string. If two hash values generated with the same hashing algorithm are different, this automatically means that the two input strings are different as well. Hashing is a one-way process&mdash;theoretically, it is impossible to determine the original string based on its hash value.

The following sections demonstrate how to add another level of security to the sample application discussed in the preceding sections by hashing user passwords. So, you will see how an existing authentication system can be modified to take advantage of hashing.

Hashing Passwords

Turning back to the testAuthor.php script discussed in the Protecting Resources Based on Information about the Authenticated User section earlier, you may notice that the cryptType parameter in the array of options is set to none, meaning that the storage container used by the Auth object will use no hashing algorithm when processing the password entered by a user, implying that the password retrieved from the database is in plain-text format as well.

Now, to start using hashed passwords in the sample application discussed in the previous section, you have to perform the following two general steps:

  • Make sure that the database sends hashed passwords instead of plain-text passwords when passing account data to the testAuthor.php script.

  • Modify the testAuthor.php script so that the Auth object hashes passwords entered via the login form.

The easiest way to make sure that the database sends password hashes when passing account data to the testAuthor.php script would be to store password hashes in the usr.accounts table, rather than storing actual passwords in clear text in this table.

However, since hashed data cannot be returned to its original state, in some situations you might want to still store user passwords in clear text, while hashing a password just before sending the user account record to which that password belongs to over the network. For example, you might want your application to support forgotten passwords functionality, sending a user his or her forgotten password via email once he or she has successfully answered a secure question.

The following figure illustrates the process of hashing a user password stored in the database in plain-text format, before sending it to the PHP script running on the web/PHP server for further processing.

Hashing Passwords

The figure depicts the process of hashing a password stored in plain-text format. The f_auth function from the sec_adm package retrieves appropriate account data from the usr.accounts table and then hashes the password in the retrieved data. Finally, it sends the updated account data to the authAuth.php script.

As you can see from the figure, in the f_auth package function you hash only the password&mdash;the rest of the account data is sent to the web/PHP server unhashed.

Before you can implement the functionality shown in the previous figure, you first have to decide which hashing algorithm to choose.

Note

As for the Oracle Database 10g Release 2, it supports several industry standard hashing algorithms, including MD4, MD5, and SHA-1. As for the PEAR::Auth package Release: 1.3.0, it uses MD5 hashing algorithm by default. Alternatively, PEAR::Auth allows you to employ any other hash function available in PHP. For example, you might use the sha1 hash function to take advantage of the SHA-1 hashing algorithm that produces a 160-bit hash.

While MD5 and SHA-1 hashing algorithms are implemented in individual PHP functions, called md5 and sha1 respectively, PHP doesn't provide a single function implementing the MD4 algorithm. To produce an MD4 hash value in PHP, you should use the hash function, specifying md4 as the first parameter and a string to be hashed as the second one. This makes using MD4 with PEAR::Auth a little tricky. Another reason not to employ MD4 is that this hashing algorithm is less secure than its successor MD5.

So, you might consider using either MD5 or SHA-1 in the example discussed here. The following table briefly summarizes these two hashing algorithms.

Hashing algorithm

Description

MD5 (Message-Digest algorithm 5)

MD5 hashing algorithm is defined in the RFC 1321 document. This algorithm produces a 128-bit hash value and is widely used to hash passwords.

SHA-1 (Secure Hash Algorithm)

SHA-1 hashing algorithm is defined in RFC 3174. It produces a 160-bit hash value. Oracle recommends using this algorithm rather than using its less secure predecessor, MD5.

The choice of the algorithm to be used is up to you. While the example in the next section uses the MD5 hashing algorithm, you could easily use SHA-1 instead.

Modifying an Authentication System to Use Hashing

Now that you have a rough idea of how hashing might be used to secure passwords in the sample application discussed in this chapter, it's time to see how to put this into action.

By executing the SQL following statements you set up database objects so that the authentication system used in the sample application uses password hashes rather than plain text passwords.

You start by altering the usr.accounts table. In particular, you enlarge the pswd column in the usr.accounts table so that it can store hash values.

You may be wondering why you need to do that&mdash;after all, you are not going to replace clear text passwords stored in this column with password hashes. Yes, you are not going to do that. However, you might refer to the Separating Security Management and Data section, where you use the structure of the sec_adm.accounts_empty_v view based on the usr.accounts table when creating a record with the account data, which is then sent to the testAuth.php script for further processing.

So, you have to make sure that the pswd column in the sec_adm.accounts_empty_v view could be used to hold a hashing value. The only way to achieve this is to alter the base table.

CONN /as sysdba
ALTER TABLE usr.accounts
MODIFY (pswd VARCHAR2(40));

After you have altered the structure of the usr.accounts table by issuing the above statement, the structure of the sec_adm.accounts_empty_v view based on that table is altered automatically. Since the sec_adm.sec_pkg package contains a reference to the sec_adm.accounts_empty_v view, the package needs to be recompiled to pick up the changes made.

Then, you recompile the package by simply recreating it.

CREATE OR REPLACE PACKAGE sec_adm.sec_pkg IS
TYPE acc_rec_set IS TABLE OF accounts_empty_v%ROWTYPE ;
FUNCTION f_auth(usrid VARCHAR2) RETURN acc_rec_set PIPELINED;
END sec_pkg;
/

Before you can start using the DBMS_CRYPTO package in your PL/SQL subprograms, you have to grant the EXECUTE privilege on this package to the database schema under which those subprograms are created.

In this particular example, you grant the EXECUTE privilege on DBMS_CRYPTO package to the sec_adm schema. This is because the f_auth function from the sec_adm.sec_pkg package will use DBMS_CRYPTO.

GRANT EXECUTE ON dbms_crypto TO sec_adm;

Next, you recreate the sec_pkg package body in order to recreate the f_auth package function so that it hashes the password before sending account data to the testAuthor.php script for further processing.

CREATE OR REPLACE PACKAGE BODY sec_adm.sec_pkg IS
FUNCTION f_auth(usrid VARCHAR2)
RETURN acc_rec_set PIPELINED IS
acc_rec accounts_empty_v%ROWTYPE;
BEGIN
SELECT * INTO acc_rec FROM usr.accounts WHERE usr_id=usrid;
acc_rec.pswd := DBMS_CRYPTO.HASH (
UTL_I18N.STRING_TO_RAW (acc_rec.pswd, 'AL32UTF8'),
DBMS_CRYPTO.HASH_MD5);
acc_rec.pswd:=NLS_LOWER(acc_rec.pswd);
PIPE ROW(acc_rec);
RETURN;
END;
END sec_pkg;
/

In the f_auth function, note the use of the HASH function from the DBMS_CRYPTO package. This function either takes a RAW or LOB value as a parameter and returns the hashed value computed using the hashing algorithm specified with the constant passed to the HASH function as the second parameter.

Since the HASH function can take only a RAW or LOB value as the parameter to be hashed, rather than taking a VARCHAR2 value, you first have to convert a VARCHAR2 to a RAW before passing it to the function. In this example, you use the UTL_I18N.STRING_TO_RAW function to convert a password stored in the database in clear text as VARCHAR2 to a RAW value.

In this example, you specify the DBMS_CRYPTO.HASH_MD5 package constant as the second parameter of the HASH function. This tells the HASH function to compute a hash value using the MD5 hashing algorithm.

Note

If you want to use SHA-1 hashing algorithm rather than MD5, you must specify the DBMS_CRYPTO.HASH_SH1 package constant instead.

Finally, you convert the hash value returned by the HASH function to lowercase. This is because the HASH function returns a hash value in uppercase, but the md5 PHP function, which is used by the PEAR::Auth class to hash a password entered via the login form, returns a hash value in lowercase.

Now, to make sure that everything works as expected, you might perform the following quick test from SQL*Plus when connected as sysdba:

SELECT * FROM TABLE(sec_adm.sec_pkg.f_auth('bob'));

The results should look as follows:

USR_ID	 FULL_NAME	 PSWD 	NUM_LOGONS
--------- --------------- ---------------------------------- ----------
bob	 Bob Robinson	 a3f05c8283e5350106829f855c93c07d	 32

As you can see, the pswd column in the output contains a hash value rather than a plain text. This means that a user password, being a sensitive part of the account data, will be sent to the web/PHP server processing the testAuthor.php script, as a 32‑character string of random-looking hexadecimal digits.

Now, you can test the updated authentication system by running the appPageInfo.php script discussed in the Performing Authorization Based on the User Identity section earlier in this chapter. Before you can do that, however, you have to turn back to the testAuthor.php source code and set the cryptType parameter in the array of options to md5, as shown below:

'cryptType'=>'md5'

Note

Alternatively, you might simply remove this parameter from the array. The fact is that PEAR::Auth uses the MD5 hashing algorithm by default, when it comes to passwords. However, if you have decided to use the SHA-1 hashing algorithm, you must explicitly set the cryptType parameter to sha1.

It is important to note that the hashing mechanism employed here works behind the scenes. From the end user standpoint, the sample application behaves as before.

Setting Up Fine-Grained Access with Database Views

There may be situations where you need to remove access to a certain column within the table because that column contains sensitive data. In such situations, using views is definitely the best way to go.

Your first step is to create a view that selects all the columns from the underlying table except the one you want to make inaccessible.

Then, you grant the SELECT privilege on that view to your users, instead of granting this privilege on the underlying table.

The following figure gives a graphical depiction of this solution.

Setting Up Fine-Grained Access with Database Views

As you can see from the figure, the simple technique based on using views allows you to restrict access to sensitive data within tables, without the need to reconstruct existing database objects.

Note

It is interesting to note that views can also be used to restrict access to certain rows in their base tables. To achieve this, you define the WHERE clause in the view's defining query so that the view displays only allowable rows of the base table or tables.

The examples provided in the following sections demonstrate how views can be used when implementing fine-grained access for each application user in a PHP/Oracle application that uses a single database schema on behalf of all users. The first example will show using views for column-level security. Then, you will see an example of implementing value-based security with views.

Implementing Column-Level Security with Views

For the purpose of the example discussed in this section, let's say that the usr.accounts table used in the previous examples contains records representing sales representatives working for your firm. Suppose you store all the orders placed by the sales representatives in the orders table, which resides in the usr database schema.

With the following SQL statements, you create the usr.orders table and populate it with data:

CONN /as sysdba
CREATE TABLE usr.orders(
ordno NUMBER PRIMARY KEY,
empno VARCHAR2(40) REFERENCES usr.accounts(usr_id),
orddate DATE,
total NUMBER(10,2)
);
INSERT INTO usr.orders VALUES
(1001, 'bob', '01-aug-2006', 5870.00);
INSERT INTO usr.orders VALUES
(1002, 'bob', '01-aug-2006', 12500.00);
INSERT INTO usr.orders VALUES
(1003, 'maya', '04-aug-2006', 1100.50);
INSERT INTO usr.orders VALUES
(1004, 'bob', '05-aug-2006', 10230.00);
COMMIT;

Now, suppose you want each sales representative to be ale to view all the orders, regardless of who placed an order, but, at the same time, you want to keep the empno column in the orders table inaccessible, so that everyone can view any order but there is no way to determine who placed it.

Next, you create a view on the usr.orders table created above, so that the newly created view contains only allowable columns from the base table.

CONN /as sysdba
CREATE OR REPLACE VIEW usr.orders_v AS
SELECT ordno, orddate, total
FROM usr.orders;
GRANT SELECT ON usr.orders_v TO app_conn;

In this example, for better security you create the order table as well as the orders_v view built upon this table in the usr database schema, and then grant SELECT privilege only on the view to the app_conn schema, which is used by application users to connect to the database.

A secure page based on the orders_v view created here might look like the following figure:

Implementing Column-Level Security with Viewsviewsdatabase access, setting up

The above page can be seen by anyone who has successfully authenticated. Containing information about the orders placed by all employees, the page doesn't provide any information on who exactly placed a certain order.

To see the page shown in the figure, you might run the appPageOrders.php script shown below:

<?php
//File: appPageOrders.php
require_once "testAuthor.php";
$infoPage='"'."appPageInfo.php".'"';
$rslt =$dbh->query("SELECT ordno, TO_CHAR(orddate,
'DD-MON-YYYY') orddate, total FROM usr.orders_v");
if (PEAR::isError($rslt)) {
print $rslt->getMessage();
}
print '<font face="Arial">';
print "<h3>List of orders placed by all employees</h3>";
print '<table border="1" cellpadding="5">';
print '<tr>';
print '<th>Order No</th><th>Order Date</th><th>Order Total</th>';
print '</tr>';
while($row = $rslt->fetchRow(DB_FETCHMODE_ASSOC)){
print '<tr>';
print '<td>'.$row['ORDNO'].'</td><td>'.$row['ORDDATE'].
'</td><td align="right">'.number_format($row['TOTAL'],
2).'</td>';
print '</tr>';
};
print '</table>';
print '<br/>';
print "Click <a href=".$infoPage.">here</a> to see the account
info related to the current user";
print '</font>';
?>

In the above script, note the use of the TO_CHAR function to convert a DATE to VARCHAR2 in the query. You pass the date format as the second parameter to the function.

After the query has completed, you can fetch the results using one of the fetch functions available in PEAR::DB. In this example, you use the fetchRow method, which is called in a loop until there are no more rows to fetch.

Finally, you define a link to another secure page of the sample, which is produced by the appPageInfo.php script discussed in the Protecting Resources Based on Information about the Authenticated User section earlier in this chapter.

Masking the Column Values Returned to the Application

Using views is definitely the best way to go if you need to remove access to a certain column or columns within the underlying table. However, in some situations, making a certain column completely inaccessible may be inappropriate. Instead, you might want to mask some values within a certain column, letting a user access only those column values that he or she is authorized to access.

Using the DECODE Function

This section discusses how the built-in DECODE function can be used to implement column masking when implementing a fine-grained access with views.

Turning back to the preceding example, suppose you want to add the full_name column from the usr.accounts table to the select list of the of the orders_v view's defining query, masking the values of that column so that each user can see the employee's name only in records representing orders placed by him or her (in other words, the user will see his or her name), while the other records should contain null in this column.

By issuing the following SQL statements, you create the orders_decode_v view whose defining query employs the DECODE function for masking the full_name column derived from the usr.accounts table.

CONN /as sysdba
GRANT EXECUTE ON sec_adm.set_cxt_pkg TO usr;
CREATE OR REPLACE FUNCTION usr.f_get_userid RETURN VARCHAR2 IS
BEGIN
RETURN sec_adm.set_cxt_pkg.get_userid;
END;
/
CREATE OR REPLACE VIEW usr.orders_decode_v AS
SELECT ordno, DECODE(empno, usr.f_get_userid, full_name, NULL) empname, orddate, total
FROM usr.orders, usr.accounts
WHERE empno=usr_id;
GRANT SELECT ON usr.orders_decode_v TO app_conn;

You start by creating a function that will return the account name of the currently authenticated user held in the userid variable of the app_cxt_pkg package in the sec_adm schema.

You create this function in the usr schema so that it can be called from within the orders_decode_v view created in the same schema, when connected to the database through the app_conn schema.

As you can see, the usr.orders_decode_v view is based on two tables, namely usr.orders and usr.accounts. The DECODE function is used in the select list of the view's query to mask values of the full_name column derived from the usr.accounts table.

The following figure illustrates what a secure page based on the usr.orders_decode_v view just created might look like.

Using the DECODE Function

While the above page displays all the orders placed by all employees, there is no way the user can determine who exactly placed a certain order unless he or she did it.

The following listing shows the source code for the appPageEmpOrders.php script that might be used to produce the page shown in the above figure.

Note

The page produced by this script will contain information about orders placed by all employees, masking the values in the empname column depending on the currently authenticated user.

<?php
//File: appPageEmpOrders.php
require_once "testAuthor.php";
$infoPage='"'."appPageInfo.php".'"';
$rslt =$dbh->query("SELECT ordno, empname, TO_CHAR(orddate,
'DD-MON-YYYY') orddate, total FROM usr.orders_decode_v");
if (PEAR::isError($rslt)) {
print $rslt->getMessage();
}
print '<font face="Arial">';
print "<h3>List of orders placed by all employees</h3>";
print '<table border="1" cellpadding="5">';
print '<tr>';
print '<th>Order No</th><th>Employee</th><th>Order Date</th><th>Order Total</th>';
print '</tr>';
while($row = $rslt->fetchRow(DB_FETCHMODE_ASSOC)){
print '<tr>';
if (is_null($row['EMPNAME'])){$row['EMPNAME']='&nbsp;';}
print '<td>'.$row['ORDNO'].
'</td><td>'.$row['EMPNAME'].'</td><td>'.$row['ORDDATE'].
'</td><td align="right">'.number_format($row['TOTAL'], 2).'</td>';
print '</tr>';
};
print '</table>';
print '<br/>';
print "Click <a href=".$infoPage.">here</a> to see the account
info related to the current user";
print '</font>';
?>

In the above script, you query the usr.orders_decode_v view created earlier in this section. Note that empname in the select list of the query is an alias for the view's select list item built on the DECODE function.

Implementing Row-Level Security with Views

In the preceding example, you saw how to restrict access to sensitive fields within the view's record based on the user's identity, while still allowing each user to view all records in the view. However, sometimes you might need to allow a user to view only the records related to his or her account, completely restricting access to all other records.

Continuing with the preceding example, suppose you want to create another page in the sample application on which each user will see only his or her orders. To achieve this, you first need to create another view on the usr.orders and usr.accounts tables, defining a predicate in the view's query that will be specific to the current user. This view will return only the records related to the currently authenticated user's account, and eliminate all the other records.

CONN /as sysdba
CREATE OR REPLACE VIEW usr.orders_emp_v AS
SELECT ordno, full_name as empname, orddate, total
FROM usr.orders, usr.accounts
WHERE (empno=usr.f_get_userid) AND (empno=usr_id);
GRANT SELECT ON usr.orders_emp_v TO app_conn;

Note the use of an alias for the second item in the select list of the view query. Here, you use the same alias you used for the second item in the select list of the usr.orders_decode_v view created as described in the preceding section. By doing so, you eliminate the need to change a lot in the appPageEmpOrders.php script when modifying it to use the usr.orders_emp_v view instead of usr.orders_decode_v.

Unlike the usr.orders_decode_v view that uses the usr.f_get_userid PL/SQL function in the view's select list as a parameter of the DECODE function, the usr.orders_emp_v view discussed here employs this function in the WHERE clause to enforce row-level security through a predicate.

A secure page based on the usr.orders_emp_v view discussed here might look like the following figure:

Implementing Row-Level Security with Views

This page displays all the orders placed by the currently authenticated user (employee). By clicking the link at the bottom of the page, a user can load a page as shown in the figure in the Using the DECODE Function section in order to see all orders placed by all employees.

The appPageEmpOwnOrders.php script shown below might be used to produce a page like that shown in the previous figure.

<?php
//File: appPageEmpOwnOrders.php
require_once "testAuthor.php";
$ordersPage='"'."appPageEmpOrders.php".'"';
$rslt =$dbh->query("SELECT ordno, empname, TO_CHAR(orddate,
'DD-MON-YYYY') orddate, total FROM usr.orders_emp_v");
if (PEAR::isError($rslt)) {
print $rslt->getMessage();
}
print '<font face="Arial">';
print "<h3>List of orders placed by
".$a->getAuthData('FULL_NAME')."</h3>";
print '<table border="1" cellpadding="5">';
print '<tr>';
print '<th>Order No</th><th>Employee</th><th>Order
</th><th>Order Total</th>';
print '</tr>';
while($row = $rslt->fetchRow(DB_FETCHMODE_ASSOC)){
print '<tr>';
print '<td>'.$row['ORDNO'].
'</td><td>'.$row['EMPNAME'].'</td><td>'.$row['ORDDATE'].
'</td><td align="right">'.number_format($row['TOTAL'], 2).'</td>';
print '</tr>';
};
print '</table>';
print '<br/>';
print "Click <a href=".$ordersPage.">here</a> to see all orders
placed by all employees";
print '</font>';
?>

As you can see, the query passed to PEAR::DB's query method here looks the same as the query in the appPageEmpOrders.php script discussed in the Using the DECODE Function section earlier. The only difference between these queries is the view specified in the FROM clause.

Since all the orders displayed on a page generated by this script are always specific to a current user, it makes sense to display a message that includes the name of that user, saying who placed the orders displayed on the page. Note the use of the $a variable, which contains a reference to the Auth object created during execution of the testAuthor.php script included at the beginning of the script. Here, you call the getAuthData method of Auth with FULL_NAME as the parameter to get the full name of the currently authenticated user.

Here, you display a link to the appPageEmpOrders.php script that produces a page displaying all orders placed by all employees and masking the empname column values depending on the currently authenticated user.

Using VPD to Implement Row-Level Security

Virtual Private Database (VPD) is a powerful Oracle Database security feature that allows you to implement row-level security, centralizing access security mechanisms within the database. The basic idea behind this approach is that each user, when successfully authenticated, is authorized to access only certain rows within a database table or view protected with a VPD security policy.

Note

The Virtual Private Database feature is available only in Oracle Database Enterprise Edition. If you're using another edition of Oracle database, consider using a view-based approach when implementing row-level security, as discussed in the preceding section.

Since a VPD policy can be applied directly to a database table and is enforced whenever data in that table is accessed (directly or indirectly), there is no way for the user to bypass security. To protect a table or view with a VPD policy, you have to perform the following general steps:

  1. Create a PL/SQL function that implements the security policy.

  2. Register the function created in step 1 against the table or view to be protected.

That is it. Once you have performed the above steps, the database will append a dynamic predicate generated by the function created in step 1 to the original SQL statement whenever you issue it against the protected table or view.

Turning back to the preceding example where you implemented a security solution to protect the information in the usr.orders_emp_v view at the row level, based on the user's identity, you might achieve the same general results using the VPD feature.

To start with, you have to recreate the usr.orders_emp_v view so that its defining query does not contain the empno=usr.f_get_userid predicate. You really don't need this predicate in the view's query because the database will automatically append a dynamic predicate to each SQL statement issued against the view once you've protected it by a VPD policy.

So, the updated view might be created as follows:

CONN /as sysdba
CREATE OR REPLACE VIEW usr.orders_vpd_v AS
SELECT ordno, full_name as empname, orddate, total
FROM usr.orders, usr.accounts
WHERE empno=usr_id;
GRANT SELECT ON usr.orders_vpd_v TO app_conn;

Here, you create the usr.orders_vpd_v view on the usr.orders and usr.accounts tables. Unlike the usr.orders_emp_v view created as described in the Using the DECODE Function section earlier, usr.orders_vpd_v doesn't use the empno=usr.f_get_userid predicate in the WHERE clause of the view's query. This is because the usr.orders base table containing sensitive information on orders will be protected by the VPD policy based on the PL/SQL function defined later in this section.

From a security standpoint, it would be a good idea in this example to apply a VPD policy to the usr.orders base table, rather than protecting the usr.orders_vpd_v view based on this table. Protecting the usr.orders table with a VPD policy guarantees that each user will be able to access only those order records that he or she is authorized to access, whether the user is using the usr.orders_vpd_v view for that, or accessing the usr.orders base table directly.

Therefore, the next step is to create a policy that can be applied to the usr.orders table to enforce row-level security based on the user's identity. To do this, you first need to create a PL/SQL function that implements the security policy, producing a dynamic predicate that will be appended to SELECT statements issued against the usr.orders table. You might create the following function:

CONN /as sysdba
CREATE OR REPLACE FUNCTION sec_adm.f_vpd_ord (D1 VARCHAR2, D2 VARCHAR2) RETURN VARCHAR2
IS
predicate VARCHAR2(1000);
BEGIN
predicate := 'empno = '''||sec_adm.set_cxt_pkg.get_userid||'''';
RETURN predicate;
END;
/

As you can see, the sec_adm.f_vpd_ord function will generate a different predicate for each user, based on the user's identity.

Once you have created the usr.f_vpd_ord function, you need to register that function against the usr.orders table, as shown below:

CONNECT /AS sysdba;
BEGIN
DBMS_RLS.ADD_POLICY ('usr', 'orders', 'ord_policy',
'sec_adm', 'f_vpd_ord', 'select'),
END;
/
GRANT EXECUTE ON sec_adm.f_vpd_ord TO app_conn;

Now, whenever you query the usr.orders table (directly or indirectly) Oracle will append a dynamic predicate generated by the sec_adm.f_vpd_ord function to the original SELECT statement.

Note

This works regardless of the way in which you query the usr.orders table. Whether you query it directly, by issuing a query against that table, or indirectly, by issuing a query against a view based on this table, the database will filter the records retrieved from the usr.orders table, based on the security policy implemented in the sec_adm.f_vpd_ord function.

To make sure that everything works as expected, you might make use of the appPageEmpOwnOrders.php script discussed in the preceding section. Before you run this script, make sure to replace orders_emp_v with orders_vpd_v in the FROM clause of the query used in the script to retrieve information on the orders placed by the current user.

When testing the updated sample application, you should notice that employing a VPD-based mechanism to secure information on the orders stored in the usr.orders table makes no difference to the sample behavior. If you login as maya/mayapwd, you should see the same page as shown in the previous figure.

It is interesting to note that you can always drop an existing VPD policy by using the DROP_POLICY procedure from the DBMS_RLS package. For example, to drop the policy created in the preceding listing, you might issue the PL/SQL block shown below:

CONNECT /AS sysdba;
BEGIN
DBMS_RLS.DROP_POLICY('usr', 'orders', 'ord_policy'),
END;
/

Note

Since the usr.orders table will be used in the examples in the following chapters, you have to perform the PL/SQL block shown in the listing before moving on.

The example discussed in this section illustrates how a VPD secure policy can be applied to SELECT statements issued against a certain database table. However, in a real-world situation, you might apply a VPD policy to INSERT, UPDATE, INDEX, and DELETE statements issued against a certain table or view. For more information on the Oracle VPD security feature, refer to Oracle Documentation: the Using Virtual Private Database to Implement Application Security Policies chapter in Oracle Database Security Guide.

Summary

If your application provides access to confidential information, you need to control access more carefully, employing different security mechanisms protecting your data from unauthorized access and/or modification.

In this chapter, you learned how to build secure PHP/Oracle applications using the security features of both PHP and Oracle in a complementary way. The chapter began with an example of how an easy-to-use authentication mechanism provided by the PEAR::Auth class could be used with Oracle database security features to secure your PHP/Oracle application. Using techniques discussed in the Securing PHP and Oracle Applications section in this chapter, you will be able to build a simple, yet effective security solution for your PHP/Oracle application, even if you are using Oracle Database Express Edition.

Then we looked at how to use hashing to protect end-user passwords stored in a database table in plain text format and how view-based security techniques could be used to implement column-level security and row-level security, based on the user's identity. Finally, you saw how Oracle's Virtual Private Database (VPD) feature available in the Enterprise Edition of Oracle Database might be used instead of view-based security techniques to provide access control at the row level, ensuring that each user can access only those records that he or she is authorized to access.

By now, you should have a solid understanding of how to build a secure PHP/Oracle application, using different techniques and technologies available for PHP/Oracle developers.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.40.43