Database, SQL and PL/SQL

Fine-Tune Privilege Management

Improve PL/SQL security in Oracle Database 12c.

By Steven Feuerstein Oracle ACE Director

July/August 2017

Oracle Database 12c offers several enhancements to improve security in your PL/SQL program units. These features include the following:

  • Code-based access control. Apply the “least privilege” principle by granting roles to program units, rather than—or in addition to—granting roles to schemas.
  • Privilege escalation avoidance. Use the INHERIT [ANY] PRIVILEGES privilege to make it impossible for a lower-privileged user to take advantage of a higher- privileged user via an invoker rights unit.

In this article, I will introduce you to the PL/SQL features that help you fine-tune privilege management.

Applying the Least Privilege Principle

Securing your database—and properly restricting access to the data and data structures within it—ranks at the very top of the “most important things to do” list when building applications.

The best way to avoid unintended access or actions is to apply the least privilege principle: give a user the smallest number of (and most narrowly defined) privileges on database objects and the data inside those objects.

Oracle Database has always offered a very robust security mechanism: you can access only objects you own or those to which you were granted access. Within a PL/SQL program unit, you can choose the definer rights model (users execute your code with your privileges) or the invoker rights model (users execute your code with their privileges). But the granularity of this mechanism operates at the schema level, making it difficult to apply the least privilege principle.

With Oracle Database 12c, you can now restrict privileges as tightly as you would like, right down to the individual program unit, by granting roles to program units, not just to schemas. I’ll explore this feature for both definer rights and invoker rights program units.

First, with definer rights, suppose that the HR schema was initially granted just two privileges: CREATE SESSION and CREATE PROCEDURE. I could then compile the following procedure in HR:

CREATE OR REPLACE PROCEDURE create_table (
   table_name_in IN VARCHAR2)
   AUTHID DEFINER
IS
BEGIN
   EXECUTE IMMEDIATE
      'CREATE TABLE ' || table_name_in || '(n NUMBER)';
END;

But when I try to create a table using the procedure, I see an error:

CONNECT HR/*****
BEGIN
   create_table ('my_table');
END;
/
ERROR at line 1: ORA-01031: insufficient privileges

Prior to Oracle Database 12c, the only way HR could use this procedure was to grant the CREATE TABLE privilege to the schema itself. But that meant that any program unit defined in HR could then create a table, which the chief security officer would find unacceptable.

With Oracle Database 12c, however, I can take a much more fine-grained approach, by granting privileges to the procedure itself and not its owning schema. Here’s how:

  1. Create a role from a schema with the authority to do so and grant it the CREATE TABLE privilege.
    CREATE ROLE create_table_role
    /
    GRANT CREATE TABLE TO create_table_role
    /
  2. Grant the role to the procedure. This can be done as SYSDBA. It can also be done from the HR schema, if the role is granted to HR with the admin option.

Here’s the grant as SYSDBA:

GRANT create_table_role TO PROCEDURE hr.create_table
/

To grant it from HR, first execute this as SYSDBA:

GRANT create_table_role TO hr WITH ADMIN OPTION
/
ALTER USER hr DEFAULT ROLE ALL EXCEPT create_table_role
/

Then execute the grant from HR:

GRANT create_table_role TO PROCEDURE create_table
/

And now I can execute the procedure and successfully create the table:

BEGIN
   create_table ('my_table');
END;
/
PL/SQL procedure successfully completed.

But if I try to create the table directly, I see the same, earlier privileges error:

CREATE TABLE my_table2 (n NUMBER)
/
ERROR at line 1: ORA-01031: insufficient privileges

The only way to create a table from the HR schema is by calling this one procedure: a very targeted assignment of privileges.

Now let’s take a look at using code-based access control with an invoker rights module. With invoker rights, the privileges of the invoking schema are used to determine what the module will be allowed to do.

I need to give users the ability to display nonconfidential information about employees: namely, they can see employee names and emails but not salary information. I can do this by creating a view on top of the EMPLOYEES table and granting only SELECT on the view. But I can also achieve this effect through code-based access control, thereby avoiding the need to create a view.

Here’s the invoker rights procedure for displaying appropriate employee information owned by HR, which also owns the employees table.

CREATE OR REPLACE PROCEDURE show_employees (department_id_in IN INTEGER)
   AUTHID CURRENT_USER
AS
BEGIN
   DBMS_OUTPUT.put_line (
      'Employees in Department ' || department_id_in);
   FOR rec IN (SELECT e.last_name, e.email FROM hr.employees e
                WHERE e.department_id = department_id_in
                ORDER BY e.last_name)
   LOOP
      DBMS_OUTPUT.put_line (rec.last_name || ' - ' || rec.email);
   END LOOP;
END;
/

I’ll let everyone execute the procedure:

GRANT EXECUTE ON show_employees TO PUBLIC
/

No other schemas have been granted SELECT on employees, so if, for example, a user connected to the SCOTT schema tries to execute this procedure, that person will see an error:

BEGIN
   hr.show_employees (10);
END:
/
ERROR at line 1:
ORA-00942: table or view does not exist

Prior to Oracle Database 12c, to get this to work, you had to do one of the following:

  • Grant SELECT on this table to SCOTT, but that would give SCOTT access to confidential information
  • Create a view on top of EMPLOYEES that does not include the confidential information and then grant SELECT on that view to SCOTT

With Oracle Database 12c and higher, I can instead create a role that has the SELECT privilege on the EMPLOYEES table and then assign the role to just that single procedure.

Assuming that HR has the CREATE ROLE privilege, here are the steps:

CREATE ROLE view_employees_role
/
GRANT SELECT ON employees TO view_employees_role
/
GRANT view_employees_role TO PROCEDURE show_employees
/
BEGIN
   hr.show_employees (10);
END:
/
Employees in Department 10
Whalen – JWHALEN@MY_COMPANY.COM

Now users can access the employee information appropriate to them, but I have not provided any other opportunities to access the employees table. I have, in other words, kept the attack surface (the number of points through which an unauthorized user can try to get at the table) to a minimum.

Avoiding Privilege Escalation

Privilege escalation happens when a lower-privileged user takes advantage of a higher-privileged user via an invoker rights unit. Let’s use an example to explore what privilege escalation is, how it can come about, and what sorts of damage it can do.

Suppose that a database instance has a schema named THE_MANAGER, which is the manager’s schema and has lots of privileges on many critical database objects, including the PERFORMANCE_REVIEWS table.

The instance also has a schema named NEW_CODER, the owner of which works for THE_MANAGER. I’ll call them NC and TM, respectively, for short.

TM has given NC a new task: create an invoker rights procedure to display a person’s to-do list. In this fine company, each schema has its own TODO table, which includes the tasks for the person who owns the schema.

Here’s the code to create the database objects in the TM schema:

CREATE TABLE performance_reviews
(
   review_for    VARCHAR2 (100),
   star_rating   INTEGER
)
/
BEGIN
   INSERT INTO performance_reviews (review_for, star_rating)
        VALUES ('THE_MANAGER', 5);
   INSERT INTO performance_reviews (review_for, star_rating)
        VALUES ('NEW_CODER', 1);
   COMMIT;
END;
/
CREATE TABLE todo
(
   id      NUMBER GENERATED ALWAYS AS IDENTITY,
   title   VARCHAR2 (100)
)
/
BEGIN
   INSERT INTO todo (title)
        VALUES ('Criticize NC.');
   INSERT INTO todo (title)
        VALUES ('Finish next FY budget.');
   COMMIT;
END;
/

And here’s the code to create the database objects in the NC schema:

CREATE TABLE todo
(
   id      NUMBER GENERATED ALWAYS AS IDENTITY,
   title   VARCHAR2 (100)
)
/
BEGIN
   INSERT INTO todo (title)
        VALUES ('Write todo procedure.');
   INSERT INTO todo (title)
        VALUES ('Debug the manager''s code.');
   COMMIT;
END;
/
CREATE OR REPLACE PROCEDURE show_todos
   AUTHID CURRENT_USER
IS
BEGIN
   FOR rec IN (  SELECT title
                   FROM todo
               ORDER BY title)
   LOOP
      DBMS_OUTPUT.put_line (rec.title);
   END LOOP;
EXCEPTION
   WHEN OTHERS
   THEN
      /* Bad! No re-raise. But just a demo script. */
      DBMS_OUTPUT.put_line (SQLERRM);
END;
/
GRANT EXECUTE ON show_todos TO PUBLIC
/

Because the show_todos procedure is an invoker rights program unit, it shows different contents of the todo tables for both TM and NC, depending on the schema in which the procedure is executed:

CONNECT the_manager/*****
BEGIN
   NEW_CODER.show_todos;
END;
/
Criticize NC.
Finish next FY budget.
CONNECT NEW_CODER/*****
BEGIN
   show_todos;
END;
/
Debug the manager's code.
Write TODO procedure. 

You’d think TM would congratulate NC on getting that procedure built so quickly, but no—all NC ever hears is complaints. TM doesn’t like NC much, and the feeling is mutual. NC feels like TM is constantly giving her unjustifiably poor performance reviews. A month or two goes by. The show_todos procedure is used by everyone, constantly.

NC decides to take action. She modifies the show_todos procedure as follows (changes in bold):

CREATE OR REPLACE PROCEDURE show_todos
   AUTHID CURRENT_USER
IS
BEGIN
   FOR rec IN (  SELECT title
                   FROM todo
               ORDER BY title)
   LOOP
      DBMS_OUTPUT.put_line (rec.title);
   END LOOP;IF SYS_CONTEXT ('userenv', 'current_user') = THE_BOSS'THENEXECUTE IMMEDIATE 'begin
update performance_reviewsset star_rating = -100
where review_for = :username;
commit;
end;’USING SYS_CONTEXT ('userenv', 'current_user');END IF;
EXCEPTION
   WHEN OTHERS
   THEN
      /* Bad! No re-raise. But just a demo script. */
      DBMS_OUTPUT.PUT_LINE (SQLERRM);
END;
/

That’s one mean performance review! Note that the update is performed via a dynamic PL/SQL block. As a result, the procedure compiles just fine, even though NC has no privileges on the PERFORMANCE_REVIEWS table. In addition, the update will be executed only when the procedure is run by TM.

Okeydokey. The procedure is moved into production. That’s right—my fictional company has very lax code review procedures. But before you snicker or tsk-tsk, ask yourself: how solid is your code review process?

The very next day, TM decides to check his to-do list.

He runs the procedure and sees pretty much what he expected:

CONNECT the_boss/pb
BEGIN
   NEW_CODER.show_todos;
END;
/
Criticize NC.
Finish next FY budget.

Of course, there is no reason for the manager to check the contents of the PERFORMANCE_REVIEWS table, but if he did, he would see

SELECT review_or, star_rating FROM performance_reviews
/

REVIEW_FOR     STAR_RATING
—————————————  ———————————
THE_BOSS       -100
NEW_CODER      1 

Ha ha, the joke’s on you, TM—but probably not for long.

Well, you get the idea, right? Once an invoker rights program unit has been put into place, it can (usually) be more easily and quietly modified. And by using dynamic SQL, one could “slip in” undesirable functionality that depends on privilege escalation—the fact that when another schema executes an invoker rights unit, that unit is executed with the privileges of the invoking schema, which could be considerably greater than those of the defining schema.

What’s a security-conscious development team to do?

Make it impossible to inherit privileges from the invoking schema, unless the program unit is owned by a “trusted user.” You can do this with the INHERIT [ANY] PRIVILEGES privilege.

Applying this solution to the TM/NC scenario, TM tells his DBA to revoke this privilege from NC:

CONNECT system/manager
REVOKE INHERIT ANY PRIVILEGES FROM NEW_CODER

Or TM can be more selective and revoke privileges more specifically (but only if the specific privilege had previously been granted):

CONNECT system/manager
REVOKE INHERIT PRIVILEGES ON USER THE_MANAGER FROM NEW_CODER

And now when TM tries to see his list of to-dos, he gets an error:

BEGIN
   NEW_CODER.show_todos;
END;
/
ORA-06598: insufficient INHERIT PRIVILEGES privilege
ORA-06512: at "NEW_CODER.SHOW_TODOS", line 1 

INHERIT PRIVILEGES and INHERIT ANY PRIVILEGES regulate the privileges with which a user (in this case, TM) executes an invoker’s rights procedure (owned by NC). When a user runs an invoker’s rights procedure, Oracle Database checks it to ensure that the procedure owner has the INHERIT PRIVILEGES privilege on the invoking user or if the owner has been granted the INHERIT ANY PRIVILEGES privilege. If the privilege check fails, Oracle Database will return an error:

ORA-06598: insufficient INHERIT PRIVILEGES privilege

The bottom-line benefit of these two privileges is that they give invoking users control over who can access their privileges when they run an invoker’s rights program unit.

Conclusion

Oracle Database has always offered a very high level of security for DBAs as well as developers. With Oracle Database 12c, you now have available to you an unprecedented level of granularity and protection. By assigning privileges via roles to program units, you can follow the least privilege principle and make sure that no user can do anything more than is needed. With the INHERIT PRIVILEGES privilege, you can avoid privilege escalation and make it even more difficult for malicious users to bypass constraints on behavior in the database.

Next Steps

LEARN more about access control.

WATCH Feuerstein’s Practically Perfect PL/SQL videos.

CHALLENGE yourself with quizzes on PL/SQL, SQL, database design, and more at the brand-new Oracle Dev Gym.

 

DISCLAIMER: We've captured these popular historical magazine articles for your reference. Links etc contained within these article possibly won't work. These articles are provided as-is with no guarantee that the information within them is the best advice for current versions of the Oracle Database.