484 Part IV: Applied Security for Oracle APEX and Oracle Business Intelligence Virtual Private Database VPD is one of the best ways to push data security down to the lowest possible level. It’s easy to envision scenarios in which all the data security was built into the application layer. When another technology is introduced that needs to access the same data, the semantics of the security policy has to be replicated to the new technology. Obviously, this is difficult to maintain, prone to errors, and easy to subvert, because all a nefarious individual has to do to access the data is connect directly to the database, effectively bypassing the security. VPD is a critically important solution to protect the data at the source. The classic VPD use case is to set one or more session context variables when an end user logs into an application, and then use those context variables in a VPD policy that determines the rows to which a user has access. This is a fairly straightforward task in client-server environments, where the database session of an end user persists as long as the user is logged into the application. As discussed in Chapter 11, APEX database sessions persist only as long as it takes to process a page request, which is typically less than a second. This is yet another area where the difference between nonpersistent and persistent sessions causes a lot of confusion for developers. Fortunately, you an integrate VPD with APEX in many ways. One option is to use the Virtual Private Database attribute of an APEX application to call a procedure that sets session context variables. This technique works particularly well with legacy VPD applications that are already using session context variables. Another option is to reference APEX items in the VPD policy, which we will refer to as an item-based policy. A third option is to use Oracle Database Global Application Context variables, which were specifically designed for use with stateless applications. Global Application Contexts have a significant drawback in that they offer no built-in way to purge expired sessions. As such, they are not a good solution for APEX and VPD, and are not covered in this book. APEX Item-based Policy The easiest approach to implement is to use APEX items. As you might expect, this approach has both advantages and disadvantages. Following are the advantages of using APEX items: APEX items are persistent for the life of the end user’s APEX session. If you set the value of an item when a user logs into the application, that value will still be there as long as the user is using the application. Because the items are persistent, their values need to be set only once—for example, when a user authenticates. This is especially beneficial if the code used to determine a user’s authorization rights is expensive. The syntax used to set an APEX item is exceptionally easy. APEX items are purged at regular intervals so a developer doesn’t need to worry about cleanup procedures. The disadvantage of using APEX items is that APEX items can be used only with APEX applications. If the VPD policy is to be used in a heterogeneous environment, it will need separate logic for APEX and any other technology that needs to access the data. ■ ■ ■ ■ Chapter 12: Secure Coding Practices in APEX 485 In the following example, we will create a simple table to store users and their roles. When a user logs into our APEX application, we will store the role in an APEX item. We will then reference the value of this APEX item in a function used for a VPD policy. If the user’s role is ADMINISTRATOR, he will see all rows in the table. If his role is READ, he will see only his own row. All others, such as a user that access this table from a reporting tool, will not see any rows. SYS@AOS> grant create any context to sec_admin; SYS@AOS> grant execute on sys.dbms_rls to sec_admin; Execute the following DDL as schema SEC_ADMIN create table sec_admin.user_app_roles( id char(32), user_name varchar2(255), role_name varchar2(255), constraint user_app_roles_pk primary key (id), constraint user_app_roles_uq unique(user_name,role_name), constraint user_app_roles_ck check (role_name in ('ADMINISTRATOR', 'READER')) ) / create or replace trigger bi_user_app_roles before insert on user_app_roles for each row begin :new.id := sys_guid(); :new.user_name := upper(:new.user_name); :new.role_name := upper(:new.role_name); end; / insert into user_app_roles (user_name,role_name) values ('DGRANT','ADMINISTRATOR'); insert into user_app_roles (user_name,role_name) values ('JWHALEN','READER'); commit; grant select on sec_admin.user_app_roles to sec_user; create or replace function sec_admin.employees_apex_item_fn ( p_schema in varchar2 default null, p_object in varchar2 default null) return varchar2 as l_return varchar2(255) := '1 = 2'; by default, this will return no rows l_role varchar2(255); begin A few basic tests to see if it looks like an APEX Session if v('APP_USER') = sys_context('userenv','client_info') 486 Part IV: Applied Security for Oracle APEX and Oracle Business Intelligence and regexp_instr(sys_context('userenv','module'), '^APEX:APPLICATION[[:space:]][0-9]+$') > 0 then ROLE_NAME is an APEX Application Level Item. Its value is set using an APEX Application Process. if v('ROLE_NAME') = 'ADMINISTRATOR' then l_return := '1 = 1'; all rows elsIf v('ROLE_NAME') = 'READER' then l_return := 'email = v('' APP_USER'')'; only their own row else l_return := '1 = 2'; no rows end if; end if; return l_return; end; / The following 2 drop statements will drop the policies used in both examples from this section to make sure we have a clean slate. begin dbms_rls.drop_policy (object_schema => 'DATA_OWNER', object_name => 'EMPLOYEES', policy_name => 'EMPLOYEES_APEX_ITEM'); end; / begin dbms_rls.drop_policy (object_schema => 'DATA_OWNER', object_name => 'EMPLOYEES', policy_name => 'EMPLOYEES_CONTEXT'); end; / begin dbms_rls.add_policy (object_schema => 'DATA_OWNER', object_name => 'EMPLOYEES', policy_name => 'EMPLOYEES_APEX_ITEM', policy_function => 'EMPLOYEES_APEX_ITEM_FN'); end; / This function EMPLOYEES_APEX_ITEM_FN references an APEX item named ROLE_NAME. Figure 12-11 shows the process that sets this item. Session Context-based Policy The next example illustrates the use of a session context variable. This technique is a more traditional approach to VPD and has an advantage in that it is applicable to other technologies such as Oracle Forms. The main disadvantage of this technique when used with APEX is that the function used to set a user’s context is called on every page view. If this function is expensive, it could have a negative impact on performance. However, if you are running Oracle Database 11g Chapter 12: Secure Coding Practices in APEX 487 or later, you can use the Function Result Cache feature to cache the results and greatly improve performance. create or replace context sec_admin.employees_context using set_employees_context / create or replace procedure sec_admin.set_employees_context as begin dbms_session.set_context('EMPLOYEES_CONTEXT','ROLE_NAME', null); for c1 in (select role_name from sec_admin.user_app_roles where user_name = sys_context('userenv','client_info')) loop dbms_session.set_context('EMPLOYEES_CONTEXT','ROLE_NAME', c1.role_name); end loop; end; / FIGURE 12-11 Application level, before header process to set the role 488 Part IV: Applied Security for Oracle APEX and Oracle Business Intelligence grant execute on sec_admin.set_employees_context to sec_user; create or replace function sec_admin.employees_context_fn ( p_schema in varchar2 default null, p_object in varchar2 default null) return varchar2 as l_return varchar2(255) := '1 = 2'; by default, this will return no rows l_role varchar2(255); begin if sys_context('EMPLOYEES_CONTEXT','ROLE_NAME') = 'ADMINISTRATOR' then l_return := '1 = 1'; all rows elsIf sys_context('EMPLOYEES_CONTEXT','ROLE_NAME') = 'READER' then l_return := 'email = sys_context(''userenv'',''client_info'')'; only their own row else l_return := '1 = 2'; no rows end if; return l_return; end; / The following 2 drop statements will drop the policies used in both examples from this section to make sure we have a clean slate. begin dbms_rls.drop_policy (object_schema => 'DATA_OWNER', object_name => 'EMPLOYEES', policy_name => 'EMPLOYEES_APEX_ITEM'); end; / begin dbms_rls.drop_policy (object_schema => 'DATA_OWNER', object_name => 'EMPLOYEES', policy_name => 'EMPLOYEES_CONTEXT'); end; / begin dbms_rls.add_policy (object_schema => 'DATA_OWNER', object_name => 'EMPLOYEES', policy_name => 'EMPLOYEES_CONTEXT', policy_function => 'EMPLOYEES_CONTEXT_FN'); end; / APEX Components Keeping in mind that each APEX page view is actually a new database session, you need to set this context with each page view. APEX provides an application-level attribute designed for this exact purpose (shown in Figure 12-12), as it occurs very early in the APEX rendering code. This is to ensure that the context is set before any application code that might need the context is executed. Chapter 12: Secure Coding Practices in APEX 489 Fine-grained Auditing The balance between security and productivity is often tough to find. Too much security, and people have a hard time doing their jobs. Too little, and you expose your system to a security breach. In many cases, developers are tasked with securing an existing or packaged application, which may limit their ability to change the underlying code or architecture. FGA is a great candidate for these applications because it’s a relatively transparent, bolt-on solution to add a layer of defense to your data. FGA allows a developer to construct an audit policy in PL/SQL and then apply that policy to database tables. Policies have access to all database environment variables of the end user’s session, such as SYS_CONTEXT variables. These environment variables allow you to define what the signature of an application user should look like, and then trigger audit events for parameters that fall outside of that signature. For example, a FGA policy might check MODULE, ACTION, IP ADDRESS, and time of day. If any of these parameters fall outside those defined by your policy, the audit event is triggered. As its name implies, this feature is only a way to audit events as they occur, but it won’t actually prevent access to the data. Consequently, FGA should be used in conjunction with other security measures that actually protect the data. Think of FGA as the silent alarm of an Oracle database. As a silent alarm, it provides an interesting deterrent to valid users of an application. Unlike techniques that hide data or raise an error, the psychological effect of this invisible tripwire can leave employees with the impression that if they start snooping around where they shouldn’t, they could initiate an investigation into their actions. Think about this in the context of a bank robbery. If a thief sets off a loud siren at night, he’ll simply run away. However, the fear of triggering a silent alarm and walking out of the bank with bags full of money to a waiting crowd of police officers provides a completely different psychological deterrent. FGA and APEX APEX is exceptionally well suited to FGA. As discussed in Chapter 11, APEX sets the environment variables MODULE and ACTION to the APEX application number and page number. To get an FIGURE 12-12 VPD application attribute 490 Part IV: Applied Security for Oracle APEX and Oracle Business Intelligence idea of the environment variables available to you in an APEX session, navigate to the APEX SQL Workshop | SQL Commands section and run the following query (shown in Figure 12-13): SELECT sys_context('USERENV', 'MODULE') module, sys_context('USERENV', 'ACTION') action , sys_context('USERENV', 'CLIENT_IDENTIFIER') CLIENT_IDENTIFIER, sys_context('USERENV', 'CLIENT_INFO') CLIENT_INFO, v('APP_USER') app_user, sys_context('USERENV', 'CURRENT_SCHEMA') CURRENT_SCHEMA, sys_context('USERENV', 'SESSION_USER') SESSION_USER, sys_context('USERENV', 'IP_ADDRESS') IP_ADDRESS FROM dual; In APEX version 3.2, the SQL Workshop is called Application 4500 and the SQL Commands feature is on page 1200. These numbers will obviously be different when you run the same query in your actual application, but they provide a nice example since the APEX development environment is also a collection of APEX applications. Notice that CURRENT_SCHEMA is the parsing schema for the query, as indicated by the select-list control in the upper-right corner of the page. This variable represents the parsing schema of your application. SESSION_USER will always be the schema that the DAD uses to connect to the database. Typically this is APEX_PUBLIC_ FIGURE 12-13 Context variable query in the SQL Workshop Chapter 12: Secure Coding Practices in APEX 491 USER for environments using Oracle HTTP Server or ANONYMOUS for environments using the Embedded PL/SQL Gateway. The IP_ADDRESS environment variable is the IP address of the client. Most people think this will be the IP address of the end user’s machine, but in an APEX environment, the database client is actually the Oracle HTTP server. This concept is useful in the context of a FGA policy designed to allow only APEX access to the data as the IP address should always be the same. However, if someone connects to the database directly with a tool such as SQL*Plus, that session’s IP address will be the IP address of the person’s PC, not the HTTP server. FGA Example 1 Now that you have a good idea of what FGA is, let’s work through a few examples that get progressively more complex. For the following examples, we’ll use two schemas: SEC_USER and SEC_ADMIN. SEC_USER will own the objects we want to audit, and SEC_ADMIN will own the functions and policies used in our auditing examples. Before we can create any policies, we must connect as a DBA and grant SEC_ADMIN execute privileges on the DBMS_FGA package. While we’re there, lets also grant select on the sys.dba_fga_audit_trail view to SEC_ADMIN. $ sqlplus / as sysdba SQL> grant execute on dbms_fga to SEC_ADMIN; Grant succeeded. SQL> grant select on sys.dba_fga_audit_trail to sec_admin; Grant succeeded. The first example uses a function called IS_APEX_SESSION_ONE that simply checks to make sure the SESSION_USER environment variable is either APEX_PUBLIC_USER or ANONYMOUS, the two most common schemas used for APEX sessions. If the SESSION_USER is considered valid, the function returns a 1 (one), or else it returns a 0 (zero). Connect as the SEC_ADMIN user and create the following function: create or replace function is_apex_session_one return number authid definer as begin if sys_context('userenv','session_user') in ('ANONYMOUS','APEX_PUBLIC_USER') then return 1; else return 0; end if; end is_apex_session_one; / Right now, this function is just a traditional function and is in no way associated with a FGA policy or a table. Execute the following code as the SEC_ADMIN user to create the policy that will tie the previous function to the table we want to audit: begin dbmS_FGA.add_policy (object_schema => 'SEC_USER', object_name => 'EMPLOYEES', 492 Part IV: Applied Security for Oracle APEX and Oracle Business Intelligence policy_name => 'IS_FROM_APEX_POLICY', audit_condition => 'SEC_ADMIN.IS_APEX_SESSION_ONE = 0', audit_column => null, statement_types => 'INSERT,UPDATE,DELETE,SELECT', enable => true); end; / In simple terms, an audit policy named IS_FROM_APEX_POLICY has been applied to the SEC_USER.EMPLOYEES table. This policy is enforced on all columns and all INSERT, UPDATE, DELETE, and SELECT statements. An audit event is triggered whenever the function IS_APEX_ SESSION_ONE returns a 0 (zero). Since the goal of this policy is to audit any queries not originating from APEX, let’s first test this by querying the EMPLOYEES table from the APEX SQL Workshop. The SQL Workshop is itself an APEX application and thus falls within the allowed parameters of our policy. Executed in the Application Express SQL Workshop select * from employees; By querying the DBA_FGA_AUDIT_TRAIL, we can determine whether or not the previous query triggered an audit event: SQL> select timestamp,db_user, client_id, object_schema,object_name, policy_name, scn, sql_text from sys.dba_fga_audit_trail; no rows selected As expected, no audit event was logged. Now, let’s run the same query once again from SQL*Plus: SQL> select * from employees; Now connect as a privileged user and query the Audit Trail table: SQL> select timestamp,db_user, policy_name, scn, sql_text from sys.dba_fga_audit_trail; TIMESTAMP DB_USER POLICY_NAME SCN SQL_TEXT 08-MAR-09 SEC_USER IS_FROM_APEX_POLICY 4638186 select * from employees FGA Example 2 For our next FGA example, we’ll leverage more session context information to narrow the allowed parameters of the audit condition a bit. This time, we will use MODULE to check that the query is coming from a specific APEX application. We can also use the SYS_CONTEXT function to determine the IP address of the client that issued the query. In this case, the expected client is our HTTP server; any other IP address indicates the query might be coming from another client such as a SQL*Plus connection from an unauthorized workstation. create or replace function is_apex_session_two return number authid definer as begin Chapter 12: Secure Coding Practices in APEX 493 if sys_context('userenv','session_user') in ('ANONYMOUS','APEX_PUBLIC_USER') and sys_context('userenv','module') = 'APEX:APPLICATION 123' and sys_context('userenv','ip_address') = '192.168.1.123' then return 1; else return 0; end if; end is_apex_session_two; / Before creating a new audit policy to associate this function with a table, we will drop the policy created in the preceding example: BEGIN DBMS_FGA.DROP_POLICY (object_schema => 'SEC_USER', object_name => 'EMPLOYEES', policy_name => 'IS_FROM_APEX_POLICY'); END; / Now we can create a new policy for this function: begin dbmS_FGA.add_policy (object_schema => 'SEC_USER', object_name => 'EMPLOYEES', policy_name => 'IS_FROM_APEX_POLICY_TWO', audit_condition => 'SEC_ADMIN.IS_APEX_SESSION_TWO = 0', audit_column => null, statement_types => 'INSERT,UPDATE,DELETE,SELECT', enable => true); end; / As you will recall, the SQL Workshop is Application 4500 and our policy is checking to make sure the query is coming from Application 123. Querying the EMPLOYEES table from the SQL Workshop should trigger an audit event: Executed in the Application Express SQL Workshop select * from employees where 1 = 1; I added the predicate 1 = 1 to show that the full SQL text is captured in the audit trail. Now query the audit trail to verify that a new event was logged: SQL> select timestamp,db_user, client_id, scn, sql_text from sys.dba_fga_audit_trail; DB_USER CLIENT_ID SCN SQL_TEXT SEC_USER 4638186 select * from employees ANONYMOUS TMUTH:3528968924651999 6009443 select * from employees where 1 = 1 . variables MODULE and ACTION to the APEX application number and page number. To get an FIGURE 12-12 VPD application attribute 490 Part IV: Applied Security for Oracle APEX and Oracle Business. sys_context('userenv','client_info') 486 Part IV: Applied Security for Oracle APEX and Oracle Business Intelligence and regexp_instr(sys_context('userenv','module'), . 484 Part IV: Applied Security for Oracle APEX and Oracle Business Intelligence Virtual Private Database VPD is one of the best ways to push data security