464 Part IV: Applied Security for Oracle APEX and Oracle Business Intelligence For this example, we’ll use DBMS_CRYPTO.RANDOMBYTES to generate a 16 byte key: SYSTEM@AOS> grant execute on dbms_crypto to sec_admin; Grant succeeded. SEC_ADMIN@AOS> select DBMS_CRYPTO.RANDOMBYTES(16) salt from dual; SALT 231F8E440E65B5C180FA184F94F55B71 Now we’ll use following table to store usernames and passwords. The user SEC_ADMIN will own this table and related packages. create table application_users( id raw(16) default sys_guid(), user_name varchar2(255), verification raw(128), constraint app_users_pk primary key (id), constraint app_users_uq unique(user_name) ) / The following package will be used to create and authenticate users. Note the use of EXECUTE IMMEDIATE for any queries or DML against the APPLICATION_USERS table. In the event that someone does gain access to our table, he cannot simply query one of the dictionary views such as DBA_DEPENDENCIES to determine the package used to set the password. This is certainly not a foolproof technique, but does make it more challenging to dissect the logic associated with password hashes. create or replace package custom_apex_auth as procedure create_user( p_username in varchar2, p_password in varchar2); function validate_user( p_username in varchar2, p_password in varchar2) return boolean; end custom_apex_auth; / create or replace package body custom_apex_auth as key from dbms_crypto.randombytes g_salt raw(256) := '231F8E440E65B5C180FA184F94F55B71'; function get_mac( p_password in varchar2) Chapter 12: Secure Coding Practices in APEX 465 return raw is begin return dbms_crypto.mac( src => utl_raw.cast_to_raw(p_password), typ => dbms_crypto.hmac_sh1, key => utl_raw.cast_to_raw(g_salt)); end get_mac; procedure create_user( p_username in varchar2, p_password in varchar2) is l_mac raw(128); begin l_mac := get_mac(p_password); execute immediate 'insert into application_users (user_name,verification) values (:a,:b)' using upper(p_username),l_mac; end create_user; function validate_user( p_username in varchar2, p_password in varchar2) return boolean is l_mac raw(128); l_user_name varchar2(255) := upper(p_username); l_count pls_integer := 0; begin l_mac := get_mac(p_password); execute immediate 'select count(*) from application_users where user_name = :username and verification = :mac ' into l_count using l_user_ name,l_mac; if l_count = 1 then return true; else return false; end if; end validate_user; end custom_apex_auth; / 466 Part IV: Applied Security for Oracle APEX and Oracle Business Intelligence Since we are storing the key inside the package, we must note that this code is accessible to anyone with a privileged account that can query data dictionary views such as DBA_SOURCE. To prevent this, we will use the PL/SQL “wrap” utility included with the Oracle Database. This utility obfuscates the code so that it is still functional, yet is not readable by an attacker. Here’s the procedure for wrapping this package: 1. Save the package body in a file named custom_apex_auth.pkb. 2. Copy this file to a computer that has the Oracle database installed. You should check for the existence of the wrap executable in $ORACLE_HOME/bin. 3. Make sure $ORACLE_HOME/bin is in your path variable. 4. Execute the following from the command line, where iname is the name of the input file and oname is the name of the output file: $ wrap iname=custom_apex_auth.pkb oname=custom_apex_auth.plb If you open the output file in a text editor, you can see that the contents are completely obfuscated: create or replace package body custom_auth wrapped a000000 1 abcd abcd abcd abcd abcd abcd abcd abcd abcd abcd abcd abcd abcd abcd abcd b 49b 2fa tEQWVnLxdhO2QpYe8q3ImRMGA2UwgzsJMUiDZ47NCjoY+Mlxa55aWhbzjdSGbS0GLgMhQ95d CYA14bY3oT+dgofd882EY0pWQou5wW4T05JazzZ4CCtLIqTZc9wBsJtEI0aEcpuUSWtLBEL8 0Em/y0eLcJoG1+pl7ZBFucjL+pHyucbrlX3UpPAHubK+mMQs9VH5b2XoZlrgpcxN41C8YZMm 8r3Brr1O2MpAu0azbDgLxlMEnvrgUO3S1XxVTNIyUJVDvvPqiTsJ98/emfxqiET2+TteElAw 28UNX7ATU3dYGJaAeUfv4ll0IVSkggDUh9oyHRsBvemuZTaXyOfD8e/2L1gKGKFGq/E95qtx jA1FuNWpKxGjpsM20NTr5TqIMs13icQ2h5et11Rv+WfFROYv6X1EI3xLeJV/JIlLPpcAkWRk Bdd71Xj45pCgOrSp37AgdOWFnzqPYiR+QRNXwXabp3muOvMOJNk5A09KshfQXTWK1mzrw7dQ qN2IRmIXQBXLXNc0kA1QfkY3/iRNfrFqLvEvoc/puVufDYElGjtRnBIJYv4qURsHG2VvIxjI Now create a user to test the code and verify that the password is not stored in clear text, and then verify that the function works as expected: Chapter 12: Secure Coding Practices in APEX 467 begin custom_apex_auth.create_user('tyler','welcome'); end; / SQL> select user_name,verification from application_users; USER_NAME VERIFICATION TYLER 02F4BB94F2C10F05F51E01B6E8A8A82928E243A8 SEC_ADMIN@AOS> set serveroutput on declare l_result boolean := false; begin l_result := custom_apex_auth.validate_user('tyler','welcome'); if l_result then dbms_output.put_line('User Authenticated'); else dbms_output.put_line('Authentication Failed'); end if; end; / User Authenticated PL/SQL procedure successfully completed. declare l_result boolean := false; begin l_result := custom_apex_auth.validate_user('tyler','hello'); if l_result then dbms_output.put_line('User Authenticated'); else dbms_output.put_line('Authentication Failed'); end if; end; / Authentication Failed PL/SQL procedure successfully completed. Before testing this code in APEX, we need to grant execute on this package from the SEC_ ADMIN schema to the SEC_USER schema. SEC_ADMIN will own the package, but our APEX application will parse as SEC_USER. SEC_ADMIN@AOS> grant execute on custom_apex_auth to sec_user; Grant succeeded. 468 Part IV: Applied Security for Oracle APEX and Oracle Business Intelligence Now that all of the code is in place, the final step is to create a new authentication scheme in APEX that leverages this code. To create a new authentication scheme in an application, navigate to Shared Components | Security | Authentication Schemes. Create a new authentication scheme, select From Scratch, and name it Custom Table-Based. Edit the newly created authentication scheme and scroll down to the Login Processing section and enter the following code in the Authentication Function attribute (as shown in Figure 12-1): return custom_apex_auth.validate_user The default login page for a new application is page 101. The next two attributes assume that your login page is also 101. If it is not, be sure to substitute the correct value of your login page. Select page 101 for the Session Not Valid attribute and make sure the Session Not Valid URL attribute is blank. Set the Logout URL attribute to the following string: wwv_flow_custom_auth_std.logout?p_this_flow=&APP_ID.&p_next_flow_page_sess=&APP_ ID.:101:&APP_SESSION. Save this authentication scheme. To activate this authentication scheme, click the Change Current tab on the Authentication Schemes page, and then select Custom Table-Based. To test the new authentication scheme, simply run the application and log in with a username of tyler and password of welcome—or any valid username and password that you created with the CUSTOM_ APEX_AUTH package. Authorization Schemes APEX authorization schemes offer a powerful and flexible solution to allow or deny access selectively to any component of an application. Their design encourages developers to define the logic for authorization at the application level and then apply that logic where it is needed. The more this code is centralized and reused, the easier it is to review, audit, and change in the event that security requirements change. To define an authorization scheme, navigate to the Shared Components section of an application. Authorization Schemes are listed in the Security section, just under Authentication Schemes. A developer can create authorization schemes based on values of APEX items, SQL queries, or PL/SQL functions. Once you create an authorization scheme and give it a unique name, this name will appear in the authorization scheme select list attribute of every APEX object that supports this feature. A partial list of APEX objects that support authorization schemes includes applications, pages, regions, report columns, list entries, and page items. FIGURE 12-1 Custom authentication scheme Chapter 12: Secure Coding Practices in APEX 469 When an authorization scheme fails for a given user, the effect differs, depending on the type of object to which it is applied. For an application or page, APEX will return an error to any user that violates the authorization scheme. All other objects, such as report columns, regions, items, and buttons, will simply not appear on the page for users that do not pass the authorization scheme. This allows you to hide navigational elements or actions that a user cannot perform. Note that simply hiding a list item or tab that links to an administrative page is not enough, however, as the user could simply enter the page in the URL. The authorization scheme should also be applied to the page. If a nefarious user tries to access a page for which she is not authorized, the APEX will display the error message associated with the authorization scheme. In the following example, we create an Authorization scheme based on a PL/SQL function. This function uses the APEX_LDAP package to query an LDAP directory and return the Organizational Unit of a person. Based on the value of this attribute, we conditionally allow access to certain parts of the application. Two users are involved in this scenario: Kathy Evans (IT Security) and Robert Smith (Human Resources). Here is the code for the authorization scheme (also shown in Figure 12-2): declare l_attributes wwv_flow_global.vc_arr2; l_attribute_values wwv_flow_global.vc_arr2; begin "ou" is the attribute we are interested in. l_attributes(1) := 'ou'; apex_ldap.get_user_attributes( p_username => :APP_USER, APEX User p_pass => null, p_auth_base => 'ou=people,dc=example,dc=com', p_host => '127.0.0.1', p_port => 389, p_attributes => l_attributes, p_attribute_values => l_attribute_values); if l_attribute_values(1) = 'Human Resources' then return true; else return false; end if; end; Here’s how to implement this code as an authorization scheme: 1. Navigate to the Shared Components section of an application. 2. Select Authorization Schemes. 3. Create a new authorization scheme with the name In HR Org. 4. Under Scheme Type, select PL/SQL Function Body Returning a Boolean. 5. Enter the code in the preceding listing in the Expression 1 attribute. 6. Provide an error message. 470 Part IV: Applied Security for Oracle APEX and Oracle Business Intelligence Now that we have created an authorization scheme named In HR Org, that name will appear in the select list of the authorization scheme attribute of almost every APEX object. Figure 12-3 shows this attribute for an APEX button. Note that APEX will also include the opposite of our authorization scheme by including “(Not In HR Org),” as this a common requirement and eliminates the need to implement it in code. FIGURE 12-2 In HR Org authorization scheme FIGURE 12-3 Authorization scheme attribute of a button Chapter 12: Secure Coding Practices in APEX 471 Let’s use the report on the EMPLOYEES table as an example. Figure 12-4 shows the same page as it appears for two different users. The user Robert Smith passes the authorization scheme, while user Kathy Evans does not. The authorization scheme was applied to the Salary and edit link columns of the report, the Create button, and the Search text box. As you can see, those elements simply do not appear for Kathy Evans. There are several scenarios in which reusing the logic of an authorization scheme is desirable, yet the declarative attribute is not present. For example, let’s say we want to have sensitive items show up as read-only for anyone who is not an administrator. This simply isn’t possible with the authorization scheme attribute of an item, as that attribute would completely hide the item for non-administrators, not simply show it as read-only. If the logic for who is considered an administrator is already defined in an authorization scheme, we can leverage that logic by calling the APEX_UTIL.PUBLIC_CHECK_AUTHORIZATION API. This API accepts the case-sensitive name of an authorization scheme and returns a Boolean, true, if the current user passes the authorization scheme, and false if he or she fails the authorization scheme. So, if we have an authorization scheme named IsAdmin, we can set the read-only attribute of one or more items to PL/SQL Function Body Returning a Boolean and enter the following code in the Expression 1 attribute: return apex_util.public_check_authorization('IsAdmin'); You can also use this API for certain circumstances in which you want to check more than one authorization scheme. If you need to check more than one authorization scheme in multiple places, it might just be easier and more efficient to code an additional authorization scheme that encompasses the logic of several other schemes. FIGURE 12-4 Impact of authorization scheme for Robert and Kathy 472 Part IV: Applied Security for Oracle APEX and Oracle Business Intelligence SQL Injection Database-driven web applications are more prevalent than ever. They are no longer relegated to the one-off internal systems of corporations, but have made their way to almost every mainstream web application on the Internet. From photo-sharing sites; to web-based e-mail applications, online auctions, and store-fronts; to content management systems—all have a database behind the scenes. New static HTML sites are now the exception, not the norm. After all, why would anyone choose the management nightmare of a bunch of loosely coupled HTML, CSS, JavaScript, and image files when a content management system can separate the content from the formatting and move the ability of editing content from a few select technologists down to the people actually responsible for the content? A database brings so much power and flexibility to the Web, but it also introduces vulnerabilities. SQL injection has been around for several years, but few developers know what it is and even fewer know how to prevent it. A SQL injection attack attempts to change the meaning of a predefined SQL query in an application. This type of attack can also change or add Data Manipulation Language (DML) statements, which are any inserts, updates, or deletes. Data Definition Language (DDL) could also be injected. Most queries that are used inside an application consist of the SQL query and one or more parameters that can be changed at runtime. For example, imagine a web page that lists all employees for a particular department. As a user changes a select list on the web page, the page is submitted and the following query is executed: select first_name,last_name,phone_number from employees where department_id = || X The only thing that changes between page views is the value of X in the predicate. The developer of this application expects that only the value for DEPARTMENT_ID will ever be a number. However, a hacker might have very different plans for this predicate, such as returning all rows, or perhaps returning metadata about the database schema to plan an attack. The key to preventing SQL injection attacks is the use of bind variables. If the structure or semantics of a query can change at runtime, then it is potentially vulnerable to SQL injection. Most of the time, the vulnerability is introduced by concatenating variables within the body of the query. The example in the preceding paragraph shows the variable X concatenated with the rest of the query. The query cannot be parsed before this concatenation occurs, and therefore the concatenation can change the structure of the query every time it is run. For our purposes, we can simplify the Oracle SQL parser a bit and assume that it goes through three phases to run a SQL query: parse, bind, and execute. In the parse phase, the SQL parser checks that the query is syntactically valid and that all the objects that it references are valid, and then it locks in the structure of the query. During the bind phase, the actual values of bind variables are substituted for their placeholders in the query. Again, the bind variables can change the value of the placeholder variables, but they cannot change the structure of the query since that was already established during the parse phase. The final step is for the SQL parser to execute the query. Queries that concatenate, not bind variables, essentially reverse the bind and parse phases. The string is concatenated together, including the values of the variables, and then it is parsed. This reversal of phases allows an attacker to change the semantics of a query completely at runtime. Chapter 12: Secure Coding Practices in APEX 473 Example 1: The Wrong Way To help you better understand the concept of SQL injection, let’s construct a procedure that is vulnerable to SQL injection. The following procedure takes in a parameter of p_last_name, then outputs all employees that match the parameter. (Note that I am using the “Q quote mechanism” introduced in Oracle Database 10g R2 to make the examples easier to read.) The key is that the string is enclosed in the following syntax: q’! some string!’. This allows strings to contain single quotes without the need to escape those single quotes. create or replace procedure sql_injection( p_last_name in varchar2) is type employee_record is table of employees%ROWTYPE; emp_rec employee_record := employee_record(); x varchar2(32767); begin x := q'!select * from employees where last_name = '!'||p_last_name||q'!'!'; execute immediate x bulk collect into emp_rec; for i in emp_rec.first emp_rec.last loop dbms_output.put(emp_rec(i).last_name||' - '); dbms_output.put_line(emp_rec(i).salary); end loop; dbms_output.put_line(emp_rec.count||' Rows Returned'); end; / At first glance, this procedure seems valid, and it would probably run just fine in a production environment. Let’s take a look at the first two examples with this procedure. They are semantically the same, though the second procedure uses the q quote mechanism. hr@aos> set serveroutput on hr@aos> exec sql_injection('Grant'); Grant - 2600 Grant - 7000 2 Rows Returned hr@aos> exec sql_injection(q'!Grant!'); Grant - 2600 Grant - 7000 2 Rows Returned As you can see, the procedure displays both employees with the last name Grant. Now suppose we want to see all employees, even though the developer of this procedure never intended this functionality: hr@aos> exec sql_injection(q'!Grant' or 1 = 1 !'); King - 24000 . 464 Part IV: Applied Security for Oracle APEX and Oracle Business Intelligence For this example, we’ll use DBMS_CRYPTO.RANDOMBYTES to generate a 16 byte key: SYSTEM@AOS>. authorization scheme for Robert and Kathy 472 Part IV: Applied Security for Oracle APEX and Oracle Business Intelligence SQL Injection Database- driven web applications are more prevalent than. this file to a computer that has the Oracle database installed. You should check for the existence of the wrap executable in $ORACLE_ HOME/bin. 3. Make sure $ORACLE_ HOME/bin is in your path variable.