Monday, May 11, 2009

Enhancing APEX Security

Note: Full explanation available here. Please read before continuing with this post.

The best way to secure data in your APEX application, or any application, is to secure your data in the database. You can do this using Oracle's Virtual Private Database (VPD), also known as Fine Grained Access Control (FGAC). APEX also has Session State Protection (SSP) which helps prevent front-end modification of data by preventing URL tampering.

I'm a big advocate of using both VPD and SSP. In some situations VPD may not be implemented for various reasons so developers must rely on SSP. SSP is great and allows developers to quickly and easily help prevent malicious users from trying to access data that they aren't supposed to. It's important to note that SSP only prevents URL tampering. If users wanted to alter data on a form they could easily do that with Firebug (I won't go into that in this post). SSP also prevents users from setting values using AJAX. This can be circumvented using the "x01" .. "x10" parameters (please see Carl's post for more info) but still leaves the door open for users to alter items in session state that they shouldn't.

How can you use SSP, make AJAX calls, and ensure that users aren't altering data on a form? This question has been bugging me for a while and I think I've come up with a solution that should work with minimal changes to existing code. For this example I'm going to use a select list containing a list of IDs (this is where the issue has come up the most for me). I'll use the emp table, with empno as the id, for this example.

Some notation before we begin: "Secure" items (i.e. ones that have a hashed value appended to them) will be called PX_ITEM_NAME_SEC while it's corresponding unsecured item is called PX_ITEM_NAME. The code only looks at "secure" items that have a matching "unsecured" item. "Unsecure" items are really items that are Hidden and Protected so they have a checksum associated to them.

The demo can be found here

Step 1- Compile Package

Since I don't have access to DMBS_CRYPTO on apex.oracle.com I've used a dummy encryption method. You'll need to grant execute on DBMS_CRYPTO from SYS:


GRANT EXECUTE ON DBMS_CRYPTO TO giffy; -- Where "giffy" is your schema name


Compile the following code in your schema

pkg_apex_sec.pks

CREATE OR REPLACE PACKAGE pkg_apex_sec
AS
/**
* Returns secure value
* @param p_value
* @return
*/
FUNCTION f_get_sec_val (
p_value IN VARCHAR2
)
RETURN VARCHAR2;

/**
* Checks if secured value is valid
* @param p_hashed_val (case sensitive
* @return 'Y' or 'N'
*/
FUNCTION is_valid_hashed_val (
p_hash IN VARCHAR2
)
RETURN VARCHAR2;

/**
* unsecure value given the hash
* @param p_hash
* @return unsecure number
*/
FUNCTION f_get_val (
p_hash IN VARCHAR2
)
RETURN VARCHAR2;

/**
* Sets unsec values in the page given the secure values
* @param p_page_id Page ID to set. Default current page
*/
PROCEDURE sp_set_page_unsec_values (
p_page_id IN apex_application_pages.page_id%TYPE DEFAULT v ('APP_PAGE_ID')
);

/**
* Set all the secure values given the unsecure values
* @param p_page_id Page ID. Default current page
*/
PROCEDURE sp_set_page_sec_values (
p_page_id IN apex_application_pages.page_id%TYPE DEFAULT v ('APP_PAGE_ID')
);
END pkg_apex_sec;


pkg_apex_sec.pkb

CREATE OR REPLACE PACKAGE BODY pkg_apex_sec
AS
-- Constants
gc_delim CONSTANT VARCHAR2 (1) := '@';

/**
* Returns hashed value
* May require sys to grant access to dbms_crypto
* - GRANT EXECUTE ON DBMS_CRYPTO TO ;
* @param p_source
* @param p_key
* @return hashed value
*/
FUNCTION f_get_md5 (
p_source IN VARCHAR2,
p_key IN VARCHAR2
)
RETURN VARCHAR2
IS
v_key VARCHAR2 (4000) := p_key;
BEGIN
-- Normally your key should be different from the source. (this is up to you to maintain)
-- For simplicity this function will see if they are the same. If so we'll append the app_id
IF p_source = p_key THEN
v_key := p_key || v ('APP_ID');
END IF;

-- Can't use DBMS_CRYPTO in apex.oracle.com. Using generic coding
-- RETURN DBMS_CRYPTO.mac (src => UTL_RAW.cast_to_raw (p_source), typ => 2, KEY => UTL_RAW.cast_to_raw (v_key));
RETURN p_source || p_key || 123; -- DELETE THIS.
END f_get_md5;

/**
* Returns secure value
* @param p_value
* @return
*/
FUNCTION f_get_sec_val (
p_value IN VARCHAR2
)
RETURN VARCHAR2
AS
BEGIN
-- For the Key value I'm arbitrarily appending the app id. You should change this to something that is secure to your code.
-- You can add :app_session as well, provided you're not using session 0 or external links (ie. from emails etc)
RETURN p_value || gc_delim || f_get_md5 (p_source => p_value, p_key => p_value || v ('APP_ID'));
END f_get_sec_val;

/**
* Checks if secured value is valid
* @param p_hashed_val (case sensitive
* @return 'Y' or 'N'
*/
FUNCTION is_valid_hashed_val (
p_hash IN VARCHAR2
)
RETURN VARCHAR2
AS
v_value VARCHAR2 (4000);
BEGIN
v_value := REPLACE (REGEXP_SUBSTR (p_hash, '^[[:print:]]+' || gc_delim), gc_delim);

IF p_hash = f_get_sec_val (p_value => v_value) THEN
RETURN 'Y';
ELSE
RETURN 'N';
END IF;
EXCEPTION
WHEN OTHERS THEN
RETURN 'N';
END is_valid_hashed_val;

/**
* unsecure value given the hash
* @param p_hash
* @return unsecure number
*/
FUNCTION f_get_val (
p_hash IN VARCHAR2
)
RETURN VARCHAR2
AS
BEGIN
IF is_valid_hashed_val (p_hash => p_hash) = 'N' THEN
RETURN NULL;
END IF;

RETURN (REPLACE (REGEXP_SUBSTR (p_hash, '^[[:print:]]+' || gc_delim), gc_delim));
EXCEPTION
WHEN OTHERS THEN
RETURN NULL;
END f_get_val;

/**
* Sets unsec values in the page given the secure values
* @param p_page_id Page ID to set. Default current page
*/
PROCEDURE sp_set_page_unsec_values (
p_page_id IN apex_application_pages.page_id%TYPE DEFAULT v ('APP_PAGE_ID')
)
AS
v_app_id apex_applications.application_id%TYPE := v ('APP_ID');
BEGIN
BEGIN
-- Set all the unsecure values from the secure values
FOR x IN (SELECT a1.item_name item_name_sec,
a2.item_name
FROM apex_application_page_items a1,
apex_application_page_items a2
WHERE a1.application_id = v_app_id
AND a1.page_id = p_page_id
AND a1.item_name LIKE '%_SEC'
-- Find corresponding item name
AND a2.application_id = a1.application_id
AND a2.page_id = a1.page_id
AND RTRIM (a1.item_name, '_SEC') = a2.item_name) LOOP
apex_util.set_session_state (x.item_name, pkg_apex_sec.f_get_val (v (x.item_name_sec)));
END LOOP;
END;
END sp_set_page_unsec_values;

/**
* Set all the secure values given the unsecure values
* @param p_page_id Page ID. Default current page
*/
PROCEDURE sp_set_page_sec_values (
p_page_id IN apex_application_pages.page_id%TYPE DEFAULT v ('APP_PAGE_ID')
)
AS
v_app_id apex_applications.application_id%TYPE := v ('APP_ID');
BEGIN
-- Set all the secure values from the secure values
FOR x IN (SELECT a1.item_name item_name_sec,
a2.item_name
FROM apex_application_page_items a1,
apex_application_page_items a2
WHERE a1.application_id = v_app_id
AND a1.page_id = p_page_id
AND a1.item_name LIKE '%_SEC'
-- Find corresponding item name
AND a2.application_id = a1.application_id
AND a2.page_id = a1.page_id
AND RTRIM (a1.item_name, '_SEC') = a2.item_name) LOOP
IF v (x.item_name) IS NOT NULL THEN
apex_util.set_session_state (x.item_name_sec, pkg_apex_sec.f_get_sec_val (v (x.item_name)));
END IF;
END LOOP;
END sp_set_page_sec_values;
END pkg_apex_sec;


Step 2- Create Application Processes

Create Application Processes (On Load Before Header) called: AP_SET_SEC_PAGE_ITEMS. This will allow you to pass IDs in the URL as you normally would.

BEGIN
pkg_apex_sec.sp_set_page_sec_values;
END;


Create Application Processes (On Submit and Before Computation ) called: AP_SET_UNSEC_PAGE_ITEMS

BEGIN
pkg_apex_sec.sp_set_page_unsec_values;
END;


Create Application Process (On Demand) called AP_NULL to set values in session (using AJAX).

BEGIN
pkg_apex_sec.sp_set_page_unsec_values; -- Update the unsec values from secure values for the current page
NULL;
END;


Step 3 - Create page with Interactive Report (IR)


SELECT *
FROM emp
WHERE empno = NVL (:p1500_empno, empno)


Step 4 - Create "Secure" Id LOV and a "Hidden and Protected" unsecured fields

Create P1500_EMPNO as Hidden and Protected. It's extremely important that you make the field Hidden and Protected so users can't alter the value in the form.
Create P1500_EMPNO_SEC as a Select List. LOV:

Instead of:

SELECT e.ename d,
e.empno r
FROM emp e


Use:

SELECT e.ename d,
pkg_apex_sec.f_get_sec_val (e.empno) r
FROM emp e


Besides adding the application processes etc, this is the only significant change that you'll need to make to your application

At this point your application is good to go. Steps 5 and 6 are included only for demonstration purposes.

Step 5 - JavaScript
This JavaScript will be used to simulate the onChange event for a select list


function onLovChange(pThisId){
var get = new htmldb_Get(null,$v('pFlowId'),'APPLICATION_PROCESS=AP_NULL',$v('pFlowStepId'));
get.add(pThisId, $v(pThisId));
vReturn = get.get();
}


Step 6 - Button
Create a Submit button that will submit and branch to the same page. This will demonstrate a "normal" submit process
Create a button called "AJAX". URL = javascript:onLovChange('P1500_EMPNO_SEC');

I haven't used this in production yet so their may be some changes that I add to the code. If you have any feedback please leave a comment.

5 comments:

  1. Martin,

    Call me slow, but I don't get how these pieces all work together.

    I'd appreciate more explanation about what your matching secure+unsecure page items accomplish. Actually, more verbiage on returning the secure value of the employee ID in the LOV would help too.

    Thanks,

    Stew

    ReplyDelete
  2. I agree with you wrt VPD and SSP and have used it extensively in my applications. However, I was shown a vulnerability with SSP that effectively renders it completely useless. Fortunately I also use VPD so I am largely covered. I would not recommend SSP to anyone until this has been fixed.

    ReplyDelete
  3. Hi Simon,

    What is the vulnerability?

    Thank you,

    Martin

    ReplyDelete
  4. It's not for public consumption, I have added you to linked-in.

    ReplyDelete