PL/SQL25 min readBy Priyanshu Pandey

PL/SQL Masterclass: Security & SQL Injection Prevention

Protect your Oracle Database from malicious attacks. Learn how to prevent SQL Injection in dynamic PL/SQL, implement DBMS_CRYPTO, and manage Invoker vs Definer rights.

Security Guide · PL/SQL Masterclass

PL/SQL Security: Defending Against SQL Injection

Dynamic SQL is powerful, but it is the number one vector for database attacks. Learn how to sanitize inputs, enforce bind variables, and correctly configure execution rights.

25 min read📅August 7, 2026✍️Priyanshu Pandey📚PL/SQL Masterclass

The Threat of SQL Injection

When you use EXECUTE IMMEDIATE to run dynamic SQL, you open the door to SQL Injection. If you directly concatenate user input into the SQL string, a malicious user can manipulate the query structure.

Consider this search procedure:

DANGEROUS! DO NOT DO THIS.
PL/SQL
PROCEDURE search_user(p_username VARCHAR2) IS
  v_sql VARCHAR2(1000);
  v_count NUMBER;
BEGIN
  v_sql := 'SELECT COUNT(*) FROM users WHERE username = ''' || p_username || '''';
  EXECUTE IMMEDIATE v_sql INTO v_count;
END;

If a hacker provides the input admin' OR '1'='1, the string becomes: SELECT COUNT(*) FROM users WHERE username = 'admin' OR '1'='1'

This bypasses the security check entirely, returning data they shouldn't see. Worse, they could inject '; DROP TABLE users; --.

Defense Strategies

The Ultimate Defense: Bind Variables

The only way to guarantee prevention of SQL injection on data values is to use Bind Variables. When you use bind variables, the database engine treats the input strictly as a literal value, never as executable code.

SAFE!
PL/SQL
PROCEDURE search_user(p_username VARCHAR2) IS
  v_sql VARCHAR2(1000);
  v_count NUMBER;
BEGIN
  -- Using :usr as a placeholder
  v_sql := 'SELECT COUNT(*) FROM users WHERE username = :usr';
  
  -- Pass the variable safely using the USING clause
  EXECUTE IMMEDIATE v_sql INTO v_count USING p_username;
END;

Even if p_username contains malicious SQL, Oracle simply searches for a user whose literal name is "admin' OR '1'='1".

Sanitizing Identifiers (DBMS_ASSERT)

Bind variables only work for values in the WHERE or VALUES clause. You cannot bind table names or column names.

If you are writing a dynamic utility that drops a table based on user input, you must sanitize the input using DBMS_ASSERT.

PL/SQL
PROCEDURE drop_table(p_table_name VARCHAR2) IS
  v_safe_name VARCHAR2(128);
BEGIN
  -- Verify the table actually exists and is a valid SQL identifier
  -- If it's malicious (e.g. "dual; drop user sys;"), this will throw an exception!
  v_safe_name := DBMS_ASSERT.SIMPLE_SQL_NAME(p_table_name);
  
  EXECUTE IMMEDIATE 'DROP TABLE ' || v_safe_name;
END;
Architecture Security

Execution Rights (AUTHID DEFINER vs CURRENT_USER)

When User A creates a package, and User B executes it, whose privileges are used?

  1. Definer's Rights (Default): The code executes with the privileges of the user who created the package. If User A has DROP ANY TABLE rights, and User B calls User A's package, User B temporarily gains the power to drop tables! This is a massive security risk if the package contains dynamic SQL.
  2. Invoker's Rights: The code executes with the privileges of the user who called the package.

To enforce Invoker's rights, add AUTHID CURRENT_USER to your package spec.

PL/SQL
CREATE OR REPLACE PACKAGE secure_admin_tools 
  AUTHID CURRENT_USER -- Code runs with the caller's permissions
AS
  PROCEDURE drop_my_table(p_table VARCHAR2);
END;
/

Data Encryption (DBMS_CRYPTO)

For storing sensitive data like passwords or API keys, never store them in plain text. Use DBMS_CRYPTO to hash or encrypt them.

PL/SQL
DECLARE
  v_password VARCHAR2(100) := 'MySecretPass123';
  v_hash RAW(256);
BEGIN
  -- Hash the password using SHA-256
  v_hash := DBMS_CRYPTO.HASH(
    src => UTL_I18N.STRING_TO_RAW(v_password, 'AL32UTF8'),
    typ => DBMS_CRYPTO.HASH_SH256
  );
  
  DBMS_OUTPUT.PUT_LINE('Hashed: ' || v_hash);
END;
/

Common Gotchas

Important Gotchas

  • !

    You cannot use bind variables in Data Definition Language (DDL) like CREATE, ALTER, or DROP. You must use concatenation, making DBMS_ASSERT absolutely mandatory.

  • !

    By default, most users do not have access to the DBMS_CRYPTO package. The DBA must explicitly run GRANT EXECUTE ON SYS.DBMS_CRYPTO TO your_user;.

Key Takeaways

Key Takeaways

  • Never concatenate user input directly into a dynamic SQL string.
  • Always use the USING clause and Bind Variables to pass values into EXECUTE IMMEDIATE.
  • Use DBMS_ASSERT to sanitize dynamic table and column names.
  • Use AUTHID CURRENT_USER on utility packages to prevent privilege escalation.
RetailCoder
All systems operational
v1.0 Live

RC:OMS

Multi-channel order management with double-entry inventory ledger. Amazon, Flipkart, Shopify, WooCommerce — one source of truth.

Launch demo →
v1.0 Live

RC:Storefront

Self-hosted headless e-commerce. Your server, your data, zero transaction fees. Native RC:OMS inventory sync.

Visit Storefront →
Pipeline

RC:Pulse

AI-powered retail analytics and demand forecasting — built natively on top of your RC:OMS and Storefront data.

Request early access →
Built in India 🇮🇳  ·  Architected by Priyanshu PandeyTalk to an engineer →