Lesson 9.4: SQL safely with eZDB
Sometimes a report or a clean-up needs a query that persistent objects cannot express: a join, a group by, an
update of many rows. eZDB runs such SQL on the configured database. It is also where most security bugs of old
extensions were: a value from a request put into SQL without escaping. In this lesson learners follow the two rules
that prevent them and learn what changes between database engines.
At a glance
| Course | Developers |
| Module | Module 9: The console and persistent objects |
| Duration | 45 minutes (20 presenting, 25 exercise) |
| Level | Advanced |
| Audience | Developers |
| Prerequisites | Lesson 9.3 |
Learning objectives
After this lesson, learners can:
- run a read query with
$db->arrayQuery()and page it withoffsetandlimit; - escape every string with
$db->escapeString()inside single quotes and cast every number with(int); - group writes with
$db->begin(),$db->commit()and$db->rollback(); - name dialect differences between SQLite, MySQL and PostgreSQL, and the limits on MongoDB;
- choose between a persistent object and raw SQL.
Trainer's notes
How to present it
Source: Database and SQL standards, sections on the eZDB rules and the dialect differences.
The two rules (5 minutes). Every string from a request, a setting or the database is escaped with
$db->escapeString( $value )inside single quotes; every number is cast with(int)before it goes into SQL. Show the example:$db = eZDB::instance(); $rows = $db->arrayQuery( "SELECT id, name FROM ezsection WHERE identifier = '" . $db->escapeString( $identifier ) . "'" . " AND id <> " . (int)$excludeId, array( 'offset' => 0, 'limit' => 10 ) );Results (3 minutes).
arrayQuery()returns an array of rows (column to value); its second argument pages the result in the engine's own syntax.query()runs a statement and returnstrueorfalse.Transactions (4 minutes).
begin()andcommit()nest by counting; only the outermost pair sendsBEGINandCOMMIT.rollback()rolls back the whole transaction. On SQLite a writer lock serializes writers.Dialects (5 minutes). Row limits, new IDs, string concatenation, substrings and
UPDATE ... JOINdiffer per engine; letarrayQuery()'soffsetandlimitand persistent objects handle what they can. On MongoDB, SQL the adapter cannot translate returns an empty result with a warning, and transactions do nothing.Choosing (3 minutes). Persistent objects first; raw SQL for reports and bulk changes, always with the two rules.
Common questions
Is escapeString() enough for a number? No: a number outside quotes cannot be "escaped around"; cast it.
Can I use prepared statements? The kernel's interface builds SQL as strings; the two rules are its protection. Follow them without exception.
Where are the security fixes of older releases described? In the 6.0.13 hardening notes linked from the database standard.
Pitfalls
LIMITwritten by hand. It differs per engine; use theoffsetandlimitparameters.- A transaction left open. A
begin()withoutcommit()orrollback()on an error path holds locks; write the rollback in the error branch. - Testing only on SQLite. A query that works on SQLite may fail on MySQL or PostgreSQL; test on the engine of the live site.
Hands-on exercise
On the development installation, with the table mysite_greeting of lesson 9.3.
Create
scripts/report.php:// Greetings per name: php bin/php/ezexec.php extension/mysite_hello/scripts/report.php $cli = eZCLI::instance(); $db = eZDB::instance(); $since = time() - 86400; $rows = $db->arrayQuery( "SELECT name, COUNT(*) AS total FROM mysite_greeting" . " WHERE created > " . (int)$since . " GROUP BY name ORDER BY total DESC", array( 'offset' => 0, 'limit' => 10 ) ); foreach ( $rows as $row ) { $cli->output( $row['name'] . ': ' . $row['total'] ); }Run it with
ezexec.php.Add a lookup by a name given in an environment variable, escaped:
$name = (string)getenv( 'GREET_NAME' ); $count = $db->arrayQuery( "SELECT COUNT(*) AS total FROM mysite_greeting WHERE name = '" . $db->escapeString( $name ) . "'" ); $cli->output( 'Greetings for ' . $name . ': ' . $count[0]['total'] );Run it with
GREET_NAME="Ada" php bin/php/ezexec.php extension/mysite_hello/scripts/report.php, then withGREET_NAME="x' OR '1'='1". Compare: the second finds no rows, because the quote is escaped.Add a transaction that renames all
Adarows and rolls back:$db->begin(); $db->query( "UPDATE mysite_greeting SET name = 'Ada L.' WHERE name = 'Ada'" ); $db->rollback();Run the report again: nothing changed.
Read the dialect table in the database standard and find how a row limit is written on PostgreSQL.
Commit the script.
Expected result
- Step 2 prints a line per name with its count.
- Step 4: a count for
Ada, and0for the injection attempt. - Step 5: the names are unchanged after the rollback.
- Step 6:
LIMIT count OFFSET offset. - The trainer asks each learner to state the two rules.
Quiz
- What are the two rules for values in SQL?
- Which method returns the rows of a
SELECT? A.query()B.arrayQuery()C.begin()D.escapeString() - How are
offsetandlimitgiven so they work on every engine? - What happens with nested
begin()andcommit()calls? - Why prefer a persistent object where one fits?
Answers
- Escape every string with
$db->escapeString()inside single quotes; cast every number with(int). - B.
arrayQuery(). - As the second argument of
arrayQuery():array( 'offset' => ..., 'limit' => ... ). - They nest by counting; only the outermost pair sends
BEGINandCOMMIT. - It escapes for you and runs on every supported database, MongoDB included.
Further reading
- Database and SQL standards.
- Security standard.
- MongoDB as the database: what raw SQL cannot do there.
Related pages
- Module 9: The console and persistent objects
- Developers: course overview
- Trainers: start here
- Free documentation: this page is free to copy, translate and improve under the GNU FDL 1.3.
Become an Exponential Partner
Partners share their information, provide funding, contribute back and develop the products with us, and that is how Exponential grows. A partner website and dashboard are planned for mid-2027: please become a partner today to speed up our growth rate and land new features faster.