{"id":106671,"date":"2025-06-06T06:24:00","date_gmt":"2025-06-06T06:24:00","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=106671"},"modified":"2025-05-15T18:30:13","modified_gmt":"2025-05-15T18:30:13","slug":"securing-postgresql-against-injection-misconfiguration-and-privilege-escalation","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/postgresql\/securing-postgresql-against-injection-misconfiguration-and-privilege-escalation\/","title":{"rendered":"Securing PostgreSQL Against Injection, Misconfiguration, and Privilege Escalation"},"content":{"rendered":"\n<p>PostgreSQL is a robust and trusted database platform, but it\u2019s not invincible. The PostgreSQL Global Development Group \u201ctakes security seriously,\u201d enabling users to trust it with mission-critical data (<a href=\"https:\/\/www.postgresql.org\/support\/security\/&quot; \\l &quot;:~:text=The%20PostgreSQL%20Global%20Development%20Group,critical%20data&quot; \\t &quot;_blank\">PostgreSQL: Security Information<\/a>). However, simply running PostgreSQL with default settings or careless practices can be like leaving your solid steel front door unlocked or even opened. You have the protection mechanisms, you just aren\u2019t using them.<\/p>\n\n\n\n<p>A very common concern is that security vulnerabilities can creep in through mistakes in how we write queries, configure the server, or especially how we manage user privileges. In fact, the PostgreSQL documentation itself emphasizes that security is a shared responsibility between the database and its environment (OS, applications, etc.) and provides guidance on how to configure and run PostgreSQL securely.<\/p>\n\n\n\n<p>In this post, I will dive into three critical areas of PostgreSQL security: <strong>SQL injection<\/strong>, <strong>misconfiguration<\/strong>, and <strong>privilege escalation<\/strong>. We\u2019ll explore how SQL injection attacks occur (with code examples in SQL and Node.js), how to harden PostgreSQL\u2019s configuration to avoid common pitfalls, and how to prevent users or attackers from escalating privileges.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-guarding-against-sql-injection-in-postgresql\">Guarding Against SQL Injection in PostgreSQL<\/h2>\n\n\n\n<p><strong>SQL injection<\/strong> is one of the most critical vulnerabilities for any database-backed application. It occurs when an attacker injects malicious SQL fragments into an application\u2019s queries, tricking the database into executing unintended commands. In PostgreSQL (as with any SQL database), if user inputs are embedded directly into SQL statements without proper handling, an attacker can modify the query\u2019s logic or execute additional commands.<\/p>\n\n\n\n<p>As one guide explains, SQL injection allows attackers to \u201csubvert the original intent\u201d of SQL statements by using <strong>unescaped parameters or destructive phrases<\/strong> (<a href=\"https:\/\/www.crunchydata.com\/blog\/preventing-sql-injection-attacks-in-postgresql&quot; \\l &quot;:~:text=SQL%20injection%20attacks%20are%20malicious,unsecured%20databases%20in%20July%202020&quot; \\t &quot;_blank\">Preventing SQL Injection Attacks in Postgres | Crunchy Data Blog<\/a>). This can lead to data theft, data tampering, or even deletion of entire tables (for example, injecting a <code>DROP TABLE<\/code> command. The fallout from such an attack is devastating to data integrity and the company&#8217;s reputation.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-how-sql-injection-happens\">How SQL Injection Happens<\/h3>\n\n\n\n<p>Consider a simple application Node.js application using the pg client (<a href=\"https:\/\/node-postgres.com\/\">node-postgres<\/a>) to authenticate users. A na\u00efve implementation might build a query by concatenating a username and password into a SQL string:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img loading=\"lazy\" decoding=\"async\" width=\"1432\" height=\"210\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/05\/a-computer-screen-with-colorful-text-ai-generated.jpeg\" alt=\"A computer screen with colorful text\n\nAI-generated content may be incorrect.\" class=\"wp-image-106672\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/05\/a-computer-screen-with-colorful-text-ai-generated.jpeg 1432w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/05\/a-computer-screen-with-colorful-text-ai-generated-300x44.jpeg 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/05\/a-computer-screen-with-colorful-text-ai-generated-1024x150.jpeg 1024w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/05\/a-computer-screen-with-colorful-text-ai-generated-768x113.jpeg 768w\" sizes=\"auto, (max-width: 1432px) 100vw, 1432px\" \/><\/figure>\n\n\n\n<p>\n  \n<\/p>\n\n\n\n<p>In this <strong>vulnerable code<\/strong>, an attacker could input alice&#8217;; &#8212; as the username. The resulting SQL would be:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"block lang:plsql\" highlight=\"true\" decode=\"true\">SELECT * FROM accounts WHERE username = 'alice'; --' AND password = 'A123@xyzhbc$^2';<\/pre><\/div>\n\n\n\n<p>The <code>--<\/code> starts a comment, causing the password check to be ignored. This query will log in the attacker as &#8220;alice&#8221; without a valid password. Worse, an attacker could try an input like <code>alice'; DROP TABLE accounts; --<\/code><em> <\/em>to terminate the query and then drop the table. PostgreSQL will happily execute both the original query and the injected <code>DROP TABLE<\/code> if the query string is sent in one go. Clearly, an injection like this can fully compromise the database.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-preventing-sql-injection-parameterization-and-validation\">Preventing SQL Injection \u2013 Parameterization and Validation<\/h3>\n\n\n\n<p>The most effective way to stop injection is to avoid concatenating untrusted input into SQL commands. Instead, use <strong>parameterized queries<\/strong> (also known as prepared statements with placeholders). Parameterization means the SQL text is sent to the database with <strong>placeholders<\/strong> for data, while the data values are sent separately.<\/p>\n\n\n\n<p>The database then safely <strong>binds<\/strong> the values to the placeholders, treating them purely as data rather than SQL (<a href=\"https:\/\/node-postgres.com\/features\/queries&quot; \\l &quot;:~:text=If%20you%20are%20passing%20parameters,code%20within%20the%20server%20itself\">Queries \u2013 node-postgres<\/a>). In node-postgres, for example, one can use <code>$1<\/code>, <code>$2<\/code>, etc. as placeholders in the query text and supply an array of values:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img loading=\"lazy\" decoding=\"async\" width=\"1264\" height=\"318\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/05\/a-computer-screen-with-text-ai-generated-content.jpeg\" alt=\"A computer screen with text\n\nAI-generated content may be incorrect.\" class=\"wp-image-106673\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/05\/a-computer-screen-with-text-ai-generated-content.jpeg 1264w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/05\/a-computer-screen-with-text-ai-generated-content-300x75.jpeg 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/05\/a-computer-screen-with-text-ai-generated-content-1024x258.jpeg 1024w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2025\/05\/a-computer-screen-with-text-ai-generated-content-768x193.jpeg 768w\" sizes=\"auto, (max-width: 1264px) 100vw, 1264px\" \/><\/figure>\n\n\n\n<p>\n  \n<\/p>\n\n\n\n<p>Here, no matter what <code>user<\/code> or <code>pass<\/code> contain (even if an attacker inputs SQL keywords or symbols), PostgreSQL will <strong>not<\/strong> treat them as SQL commands. The node-postgres driver sends the query text and data separately to the server, and the server\u2019s \u201cbattle-tested\u201d parameter substitution logic ensures the values cannot alter the SQL syntax.<\/p>\n\n\n\n<p>In other words, $1 and $2 act as safe placeholders \u2013 the data <code>'alice'; DROP TABLE accounts;--<\/code> would be bound as a string literal, not as executable code. This approach is strongly recommended: <em>\u201cavoid string concatenating parameters into the query text\u2026 (as it) often leads to SQL injection vulnerabilities\u201d<\/em> (<a href=\"https:\/\/node-postgres.com\/features\/queries&quot; \\l &quot;:~:text=If%20you%20are%20passing%20parameters,code%20within%20the%20server%20itself\">Queries \u2013 node-postgres<\/a>). When using parameterized queries, even complex inputs or quotes are handled correctly by the database driver.<\/p>\n\n\n\n<p>In addition to placeholder binding, always perform input validation on the application side. Ensure that inputs match the expected format and type (e.g., if an ID should be numeric, reject non-numeric input before ever sending them to the database. Parameterization will handle the low-level safety, but validation adds an extra layer of defense and can prevent logically invalid or malicious data from reaching your queries (<a href=\"https:\/\/www.crunchydata.com\/blog\/preventing-sql-injection-attacks-in-postgresql&quot; \\l &quot;:~:text=As%20an%20added%20precaution%2C%20parameterized,%E2%80%99%2C%20or%20a%20semicolon\">Preventing SQL Injection Attacks in Postgres | Crunchy Data Blog<\/a>).<\/p>\n\n\n\n<p>For example, you might enforce that usernames contain only alphanumeric characters or use an allowlist (whitelist) of acceptable values for certain inputs. As a second layer, you can also filter out or disallow obviously dangerous substrings like &#8212; or ; in inputs that should never contain them, though relying solely on blacklisting bad patterns is not foolproof. The primary defense should remain using placeholders or ORM query builders that do so automatically.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-avoiding-dynamic-sql-injection\">Avoiding Dynamic SQL Injection<\/h3>\n\n\n\n<p>Most SQL injection in applications comes from improperly concatenating strings in application code but be aware that similar risks exist inside the database if you construct dynamic SQL in server-side functions. PostgreSQL functions in PL\/pgSQL or other languages can execute dynamic commands (via <code>EXECUTE<\/code> in PL\/pgSQL).<\/p>\n\n\n\n<p>If these commands incorporate user input, you must escape or quote it properly using functions like <code>quote_literal()<\/code> for values and <code>quote_ident()<\/code> for identifiers or use PL\/pgSQL\u2019s <code>USING<\/code> clause to bind variables in <code>EXECUTE<\/code>.<\/p>\n\n\n\n<p>The PostgreSQL documentation warns: <em>\u201cto prevent SQL injection attacks, SQL identifiers must be escaped when they are received from an untrustworthy source.\u201d<\/em> (<a href=\"https:\/\/www.postgresql.org\/docs\/current\/libpq-exec.html&quot; \\l &quot;:~:text=Tip\">PostgreSQL: Documentation: 17: 32.3. Command Execution Functions<\/a>) This is the server-side equivalent of parameterizing queries. In short, whether in application code or in a PostgreSQL function, never directly interpolate user-provided strings into SQL commands.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-key-takeaways-to-prevent-injection\">Key Takeaways to Prevent Injection<\/h3>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li style=\"padding-top:0;padding-bottom:0\"><strong>Use Parameterized Queries:<\/strong> Always use prepared statements or parameter placeholders ($1, $2, etc.) instead of building SQL strings directly. Most PostgreSQL client libraries (including Node.js, Python, Java, etc.) support parameterization \u2013 use it consistently. <\/li>\n\n\n\n<li><strong>Input Sanitization and Validation:<\/strong> Rigorously validate and sanitize inputs in your application. Reject or cleanse anything that doesn\u2019t meet criteria (e.g., length limits, allowed characters). This reduces the chance that dangerous payloads even reach your query layer. <\/li>\n\n\n\n<li><strong>No Direct String Concatenation:<\/strong> Rarely if ever concatenate raw input into an SQL command. If you absolutely must construct SQL dynamically (for example, dynamic <code>ORDER BY<\/code> or table names), use safe helper functions. For instance, in Node you might use the <a href=\"https:\/\/www.npmjs.com\/package\/pg-format\">pg-format<\/a> library (which properly escapes identifiers) for those rare cases, or in SQL use <code>format()<\/code> with <code>%I<\/code> for identifiers and <code>%L<\/code> for literals. <\/li>\n\n\n\n<li><strong>Least Privilege for Application User:<\/strong> Even though not a direct prevention of injection, it\u2019s worth noting that you should run your application with a database role that has only the necessary privileges. If an injection does occur, a low-privileged account should not be able to do much damage. For example, the application user should not be able to execute <code>DROP all tables<\/code> or alter data it doesn\u2019t own. We\u2019ll discuss this more in the Privilege Escalation section, but it\u2019s a critical safety net. <\/li>\n\n\n\n<li><strong>Monitor and Audit:<\/strong> Keep an eye on your database logs for suspicious queries (e.g., odd <code>OR 1=1<\/code> conditions or batches of failed logins). PostgreSQL\u2019s logging can be tuned (via <code>log_statement<\/code> and related settings) to record all queries, which can help detect an injection attempt early. <\/li>\n<\/ul>\n<\/div>\n\n\n<p>By diligently coding with safe query practices and input validation, you can practically eliminate SQL injection risk in PostgreSQL. The next step is ensuring that the database itself is configured securely, so that even a misconfigured server does not undermine the best application code.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-hardening-postgresql-configuration-avoiding-misconfiguration\">Hardening PostgreSQL Configuration (Avoiding Misconfiguration)<\/h2>\n\n\n\n<p>Misconfiguration of PostgreSQL can create security holes even if your application is written securely. \u201cMisconfigurations in PostgreSQL can serve as an entry point for attackers\u201d (<a href=\"https:\/\/www.crowdstrike.com\/en-us\/blog\/securing-postgresql-from-cryptojacking-campaigns-in-kubernetes\/#:~:text=Kubernetes%20www,have%20taken%20advantage%20of\" target=\"_blank\" rel=\"noopener\">Securing PostgreSQL from Cryptojacking Campaigns in Kubernetes<\/a>), especially if they allow unauthorized connections or weak authentication.<\/p>\n\n\n\n<p>In this section we discuss common PostgreSQL configuration pitfalls \u2013 particularly in the <code>postgresql.conf<\/code> and <code>pg_hba.conf<\/code> files \u2013 and how to fix any mistakes you may have made. The focus in this section is on <strong>authentication methods<\/strong>, <strong>network exposure<\/strong>, and other default settings that need tightening.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-secure-authentication-amp-pg-hba-conf\"><strong>Secure Authentication &amp; pg_hba.conf<\/strong><\/h3>\n\n\n\n<p>PostgreSQL uses the <code>pg_hba.conf<\/code> (host-based authentication) file to control how clients can connect \u2013 which IPs, which users, and what authentication method is required. A common misconfiguration is leaving this too permissive. By default, PostgreSQL (especially older versions) may allow connections using the trust method on certain interfaces, meaning no password is required at all (<a href=\"https:\/\/www.mydbops.com\/blog\/postgresql-security-hardening#:~:text=One%20of%20the%20most%20common,attackers%20to%20gain%20unauthorized%20access\" target=\"_blank\" rel=\"noopener\">PostgreSQL Security Hardening: Best Practices to Protect Your Database<\/a>).<\/p>\n\n\n\n<p>While convenient for initial setup or local testing, trust authentication is dangerously insecure if enabled on any interface that others can access. Using trust is \u201cthe worst thing you can do\u201d on a production server (<a href=\"https:\/\/www.cybertec-postgresql.com\/en\/postgresql-security-things-to-avoid-in-real-life\/&quot; \\l &quot;:~:text=What%20we%20frequently%20see%20is,trust%20is%20certainly%20not&quot; \\t &quot;_blank\">PostgreSQL Security: 12 rules for database hardening<\/a>). An attacker who can reach the database port can instantly log in without credentials if a trust rule covers their connection.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-disable-trust\">Disable Trust<\/h4>\n\n\n\n<p>You should never use <code>trust<\/code> for remote connections, and even for local connections it&#8217;s better to use more secure methods (like <code>peer<\/code> for local sockets or a password method). Instead, configure <code>pg_hba.conf <\/code>to use strong password-based authentication (preferably <a href=\"https:\/\/www.postgresql.org\/docs\/current\/auth-password.html\">SCRAM-SHA-256<\/a>) or other methods like certificate authentication for all users. For example, a secure <code>pg_hba.conf<\/code> entry might look like:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"block\" highlight=\"false\" decode=\"true\"># Require scram-sha-256 auth for any user connecting to database \"mydb\" from the app server subnet\nhost    mydb    all    192.168.10.0\/24    scram-sha-256<\/pre><\/div>\n\n\n\n<p>This ensures a password (hashed with SCRAM) is always needed. Avoid catch-all lines like <code>host all all 0.0.0.0\/0 trust<\/code><em> <\/em>\u2013 that would allow anyone from anywhere in the world to connect with no password! In fact, avoid using <code>0.0.0.0\/0<\/code> (all IPs) in <code>pg_hba.conf<\/code> whenever possible. It\u2019s much safer to restrict access to specific IP ranges or trusted networks that your application servers reside in (<a href=\"https:\/\/www.mydbops.com\/blog\/postgresql-security-hardening#:~:text=,unauthorized%20access%20from%20unknown%20sources\">PostgreSQL Security Hardening: Best Practices to Protect Your Database<\/a>). Similarly, avoid overly broad user or database specifications; lock it down to the minimum needed (e.g., if only one database and a couple of roles are used by the app, specify those rather than \u201call\u201d). Each <code>pg_hba.conf<\/code> line should be as specific as practical about the connection it permits.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-enforce-adequate-authentication-to-servers\">Enforce adequate authentication to servers<\/h4>\n\n\n\n<p>Enforce strong passwords and authentication methods. PostgreSQL supports multiple auth methods: the older MD5-based challenge and the newer SCRAM-SHA-256 (as of PostgreSQL 10+). MD5 is considered weak by today\u2019s standards \u2013 it\u2019s vulnerable to hash cracking if an attacker intercepts the hash, especially since many common MD5-hashed passwords can be brute-forced or looked up.<\/p>\n\n\n\n<p>SCRAM-SHA-256 is a much stronger mechanism and is recommended for password authentication (<a href=\"https:\/\/www.mydbops.com\/blog\/postgresql-security-hardening#:~:text=Password,is%20recommended%20for%20added%20protection\">PostgreSQL Security Hardening: Best Practices to Protect Your Database<\/a>). Ensure your PostgreSQL is configured to use SCRAM (set <code>password_encryption = scram-sha-256<\/code> in <code>postgresql.conf<\/code> so new passwords use SCRAM by default) and update any old user passwords still stored with MD5. In <code>pg_hba.conf<\/code>, use <code>scram-sha-256 <\/code>as the method (or md5 if you must support older clients, but note it will allow MD5 or SCRAM depending on user password type).<\/p>\n\n\n\n<p>The bottom line: do not leave weak or no-auth pathways open. Every connection should require proper credentials or certificates.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-network-exposure-listen-addresses-and-firewalls\"><strong>Network Exposure \u2013 listen_addresses and Firewalls:<\/strong> <\/h3>\n\n\n\n<p>Another common misconfiguration is exposing the database to the entire network (or Internet) when unnecessary. The <code>listen_addresses<\/code> parameter in <code>postgresql.conf<\/code> controls which network interfaces PostgreSQL listens on. This is often localhost only by default, but sometimes admins set it to <code>*<\/code> (all interfaces) for convenience.<\/p>\n\n\n\n<p>Listening on all interfaces is acceptable only if you have properly restricted <code>pg_hba.conf<\/code> and external firewall rules; otherwise, it enlarges your attack surface. As a security hardening rule: don&#8217;t open PostgreSQL to more networks than needed.<\/p>\n\n\n\n<p>If your application runs on the same machine or a small set of hosts, restrict <code>listen_addresses<\/code> to those (e.g. <code>listen_addresses = 'localhost,10.0.0.5'<\/code> for a specific app server) (<a href=\"https:\/\/www.cybertec-postgresql.com\/en\/postgresql-security-things-to-avoid-in-real-life\/#:~:text=Recommendation%20to%20avoid%20listening%20on,all%20devices\" target=\"_blank\" rel=\"noopener\">PostgreSQL Security: 12 rules for database hardening<\/a>). If you listen on <code>*<\/code> and have a misconfigured <code>pg_hba<\/code> line, a bad actor could easily spam you with authentication requests \u2013 disaster is just one <code>pg_hba.conf<\/code> entry away. In essence, limiting the network exposure gives you an extra safety net: PostgreSQL won&#8217;t even see connection attempts from unauthorized sources.<\/p>\n\n\n\n<p>Bottom Line: Always use a firewall to block the PostgreSQL port (5432 by default) from any IPs that don&#8217;t need to reach it.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-require-ssl-tls-for-connections\"><strong>Require SSL\/TLS for Connections<\/strong><\/h3>\n\n\n\n<p>By default, PostgreSQL allows unencrypted connections, which could let an attacker snoop on data in transit (especially on a public network). Enabling SSL\/TLS in PostgreSQL ensures encrypted traffic between the client and server. This prevents <a href=\"https:\/\/www.postgresql.org\/docs\/current\/auth-password.html\">eavesdropping<\/a> or <a href=\"https:\/\/en.wikipedia.org\/wiki\/Man-in-the-middle_attack\">man-in-the-middle<\/a> attacks that could steal credentials or data. On the server, set <code>ssl = on<\/code> (and configure the certificate files), and in <code>pg_hba.conf<\/code>, you can use the <code>hostssl<\/code> directive to require <code>SSL<\/code> for certain connections. For example:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"block\" highlight=\"false\" decode=\"true\"># Only allow connections over SSL from the web app network\nhostssl   mydb   appuser   10.0.0.0\/24   scram-sha-256<\/pre><\/div>\n\n\n\n<p>This entry means non-SSL connection attempts will be rejected for that rule (the client must use TLS). If you&#8217;re in an environment where clients should always use encryption (which is, in most cases, in production), you might consider setting <code>sslmode=require<\/code> or <code>sslmode=verify-full<\/code> on the client side as well.<\/p>\n\n\n\n<p>Unencrypted connections in a local trusted network might be acceptable, but as a rule, <strong>encrypt data in transit<\/strong>, especially if any part of the connection goes over unsecured networks (<a href=\"https:\/\/www.mydbops.com\/blog\/postgresql-security-hardening&quot; \\l &quot;:~:text=2&quot; \\t &quot;_blank\">PostgreSQL Security Hardening: Best Practices to Protect Your Database<\/a>).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-other-configuration-best-practices\"><strong>Other Configuration Best Practices<\/strong> <\/h3>\n\n\n\n<p>There are a few more settings and practices to keep in mind:<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><strong>File Permissions:<\/strong> Ensure your PostgreSQL data directory and configuration files are inaccessible to others on the system. The default install usually sets proper permissions (e.g., pg_hba.conf should be readable\/writable only by the PostgreSQL OS user, 0600 or 0640 permissions). The wrong file permission could let an OS-level user maliciously alter your configurations (<a href=\"https:\/\/www.mydbops.com\/blog\/postgresql-security-hardening&quot; \\l &quot;:~:text=PostgreSQL%20Security%20Hardening%3A%20Best%20Practices,Unpatched&quot; \\t &quot;_blank\">PostgreSQL Security Hardening: Best Practices to Protect Your &#8230;<\/a>). <\/li>\n\n\n\n<li><strong>Remove or Secure Default Roles:<\/strong> PostgreSQL comes with a default superuser role (often Postgres). If remote login is allowed, make sure this role has a strong password, or consider disabling remote login for it entirely (e.g., in pg_hba.conf, only allow the Postgres role via local socket or from specific admin IPs). It\u2019s wise to create separate less-privileged roles for applications rather than using the Postgres superuser for everything. <\/li>\n\n\n\n<li><strong>Regular Updates:<\/strong> Keep PostgreSQL updated with the latest minor version patches. Security fixes are often delivered in minor version updates (which don\u2019t change features). Failure to apply updates can expose you to known vulnerabilities (<a href=\"https:\/\/www.mydbops.com\/blog\/postgresql-security-hardening&quot; \\l &quot;:~:text=4&quot; \\t &quot;_blank\">PostgreSQL Security Hardening: Best Practices to Protect Your Database<\/a>). For example, if an SQL injection or privilege escalation flaw is discovered in PostgreSQL itself, the fix will come with an update \u2013 you\u2019ll want to install that promptly. <\/li>\n\n\n\n<li><strong>Logging and Auditing:<\/strong> As mentioned earlier, enable sufficient logging to audit connections and statements. At a minimum, log failed login attempts and consider logging all statements (with care for performance and log volume) (<a href=\"https:\/\/www.crunchydata.com\/blog\/preventing-sql-injection-attacks-in-postgresql&quot; \\l &quot;:~:text=Adjust%20default%20settings%20in%20PostgreSQL&quot; \\t &quot;_blank\">Preventing SQL Injection Attacks in Postgres | Crunchy Data Blog<\/a>). PostgreSQL also has an extension pg_audit for more detailed auditing if needed. These can help detect misconfiguration abuse (like repeated connection attempts from unknown IPs, indicating someone found an open port). <\/li>\n<\/ul>\n<\/div>\n\n\n<p>Hardening authentication and network settings drastically reduces the chance of unauthorized access. But even authorized users (or attackers who get in) should be limited in what they can do.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-preventing-privilege-escalation-in-postgresql\">Preventing Privilege Escalation in PostgreSQL<\/h2>\n\n\n\n<p>Privilege escalation in PostgreSQL refers to a scenario where a user gains privileges beyond what was intended \u2013 for example, a low-privileged user managing to obtain superuser rights or using a misconfiguration to perform actions as a more privileged role. PostgreSQL\u2019s role-based access control is very flexible, but it can be abused with missteps. We will look at risks around the <code>SUPERUSER<\/code> role, role grants, and the use of functions that can act with elevated privileges.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-the-superuser-role-handle-with-extreme-care\"><strong>The SUPERUSER Role \u2013 Handle with Extreme Care<\/strong><\/h3>\n\n\n\n<p>In PostgreSQL, a superuser is all-powerful \u2013 it \u201cbypasses all permission checks\u201d (<a href=\"https:\/\/www.postgresql.org\/docs\/current\/role-attributes.html#:~:text=superuser%20status\" target=\"_blank\" rel=\"noopener\">PostgreSQL: Documentation: 17: 21.2. Role Attributes<\/a>). This is analogous to a root user in Linux.<\/p>\n\n\n\n<p>Suppose an attacker can make themselves a superuser. In that case, the game is over: they can drop or alter any data, create functions to read\/write the server\u2019s filesystem, or even execute shell commands via extensions or <code>COPY<\/code> commands. Therefore, keeping the superuser role closely guarded is the number one rule.<\/p>\n\n\n\n<p>According to PostgreSQL\u2019s documentation, superuser is a \u201cdangerous privilege and should not be used carelessly; it is best to do most of your work as a role that is not a superuser.\u201d (<a href=\"https:\/\/www.postgresql.org\/docs\/current\/role-attributes.html#:~:text=superuser%20status\" target=\"_blank\" rel=\"noopener\">PostgreSQL: Documentation: 17: 21.2. Role Attributes<\/a>) In practice, this means not running<strong> <\/strong>your application or routine queries as a superuser.<\/p>\n\n\n\n<p>The application should connect with a lesser role that has only the rights it needs (e.g., read\/write to specific tables). The <code>superuser<\/code> account (like <code>postgres<\/code>) should be reserved for administrative tasks and seldom used, especially not exposed to end-users or applications. By limiting the usage of superuser, you greatly reduce the risk that an SQL injection (or other bug) in the app can perform irreparable harm. For instance, if an app runs as a role that cannot <code>DROP TABLE<\/code> or cannot create new roles, then even if compromised, those actions are off-limits.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-role-privileges-and-grants\"><strong>Role Privileges and Grants<\/strong><\/h3>\n\n\n\n<p>Beyond the superuser, PostgreSQL roles can have other powerful attributes like <code>CREATEROLE<\/code> (to create different users), <code>CREATEDB<\/code> (to create databases), or membership in other roles. Mismanagement here can lead to privilege escalation. A classic mistake is granting a role more rights than necessary or misusing the <code>PUBLIC<\/code> role. By default, every PostgreSQL role is a member of a pseudo-role <code>PUBLIC<\/code>, which automatically grants some rights to everyone (for example, <code>CONNECT<\/code> on all databases and <code>USAGE<\/code> on the public schema).<\/p>\n\n\n\n<p>In older PostgreSQL versions (before 15), a newly created user could connect to any database and even create objects in the public schema of that database by default (<a href=\"https:\/\/www.cybertec-postgresql.com\/en\/postgresql-security-things-to-avoid-in-real-life\/#:~:text=Now%2C%20we%20have%20identified%20two,key%20problems\" target=\"_blank\" rel=\"noopener\">PostgreSQL Security: 12 rules for database hardening<\/a>). This means if you create a user for one database, they might still poke around in another database (though they can\u2019t read data they don\u2019t have privileges for, they could create temp objects or attempt certain operations).<\/p>\n\n\n\n<p>More worryingly, if a malicious user can create objects in a schema that some higher-privileged user later uses, it opens the door to sneaky attacks. It has been observed that most of the privilege escalation attack vectors in PostgreSQL over the years work by creating malicious objects in a database. For example, an attacker with the ability to create a function in a schema might create a Trojan-horse function (or operator) that a <code>superuser<\/code> inadvertently calls, thereby executing arbitrary SQL as the superuser.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-principle-of-least-privilege\"><strong>Principle of Least Privilege <\/strong><\/h3>\n\n\n\n<p>The principle of least privilege says each role should only have the permissions it requires. Do not grant broad privileges or make every role a member of an admin role. Start by revoking unnecessary default grants.<\/p>\n\n\n\n<p>It\u2019s a good practice to revoke <code>CONNECT<\/code> on databases and <code>CREATE<\/code> on a public schema from <code>PUBLIC<\/code> and then explicitly grant access to roles that need it (<a href=\"https:\/\/www.cybertec-postgresql.com\/en\/postgresql-security-things-to-avoid-in-real-life\/&quot; \\l &quot;:~:text=,spam%20the%20public%20schema&quot; \\t &quot;_blank\">PostgreSQL Security: 12 rules for database hardening<\/a>). For instance, if you have a database <code>appdb<\/code> that only the role <code>appuser<\/code> and admins should use, you can do:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"block lang:plsql\" highlight=\"true\" decode=\"true\">REVOKE CONNECT ON DATABASE appdb FROM PUBLIC;\nGRANT CONNECT ON DATABASE appdb TO appuser;\n\n-- Similarly, revoke schema usage\/creation and grant to specific roles:\nREVOKE ALL ON SCHEMA public FROM PUBLIC;\nGRANT USAGE ON SCHEMA public TO appuser;\nGRANT CREATE ON SCHEMA public TO appuser;  -- if appuser needs to create tables<\/pre><\/div>\n\n\n\n<p>The above ensures that random roles can\u2019t connect to <code>appdb<\/code> or create objects in its public schema. Next, <strong>a<\/strong>void giving out roles like <code>CREATEROLE<\/code> or <code>CREATEDB<\/code> to users that don\u2019t absolutely need them. A user with <code>CREATEROLE<\/code> could create new roles (though they cannot make them superusers without being superusers, they could create roles with wider grants or passwords and potentially phish an admin to use those). A user with <code>CREATEDB<\/code> could create a new database and potentially attempt to escalate by creating extensions or configuration objects (note that creating most extensions or setting certain parameters still requires a superuser, but it\u2019s best not to give that chance unnecessarily).<\/p>\n\n\n\n<p><strong>Regularly review role memberships<\/strong> as well. If you grant roles to other roles (group roles), ensure no chain unintentionally gives a regular user an indirect path to higher privileges. For example, if role A is an admin and role B is a member of A, then anyone who can assume role B effectively gets A\u2019s privileges (<code>SET ROLE<\/code> in SQL).<\/p>\n\n\n\n<p>Keep role inheritance and membership simple and tightly controlled.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-security-definer-functions-and-privilege-elevation\"><strong>Security Definer Functions and Privilege Elevation<\/strong><\/h3>\n\n\n\n<p>PostgreSQL allows you to create functions that run with the privileges of the function\u2019s owner rather than the caller. These are declared with <code>SECURITY DEFINER<\/code>. They are useful for allowing lower-privileged users to perform specific higher-privileged actions carefully. However, if not written safely, they can be abused to escalate privileges.<\/p>\n\n\n\n<p>The danger is that a malicious user might trick a <code>SECURITY DEFINER<\/code> function (owned by a <code>superuser<\/code>) into executing something unintended on their behalf. The PostgreSQL manual warns: \u201ccare is needed to ensure that the [security definer] function cannot be misused. For security, <code>search_path<\/code> should be set to exclude any schemas writable by untrusted users.<\/p>\n\n\n\n<p>This prevents malicious users from creating objects (e.g., tables, functions, operators) that mask objects intended to be used by the function.\u201d (<a href=\"https:\/\/www.cybertec-postgresql.com\/en\/abusing-security-definer-functions\/&quot; \\l &quot;:~:text=The%20documentation%20warns%20of%20the,dangers&quot; \\t &quot;_blank\">Abusing SECURITY DEFINER functions in PostgreSQL<\/a>). In simpler terms, if you create a <code>SECURITY DEFINER<\/code> function, you should <strong>hard-code the <\/strong><code>search_path<\/code> (or qualify object names) so that an attacker cannot create a fake table or function in a schema that comes earlier in the path and have your definer function call that instead.<\/p>\n\n\n\n<p>Additionally, carefully validate any inputs to security-definer functions \u2013 do not pass user input to <code>EXECUTE<\/code> without thorough sanitization. Ideally, keep the logic simple and avoid dynamic SQL in such functions. And of course, restrict who can execute the function: use <code>GRANT EXECUTE ON FUNCTION<\/code> to limit it to the roles that truly need it.<\/p>\n\n\n\n<p>If you follow the above practices, the need for <code>SECURITY DEFINER<\/code> functions can be minimized. But when you do use them, they should be short, audited, and safe. Another tip: from PostgreSQL v15 onward, new databases no longer grant <code>CREATE<\/code> on the public schema to <code>PUBLIC<\/code> by default (closing one avenue of attack with malicious objects).<\/p>\n\n\n\n<p>But if you\u2019re on older versions or upgraded from them, you may need to revoke those privileges manually, as shown earlier. Keep an eye on PostgreSQL release notes for security-related changes like this.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-practical-tips-to-prevent-privilege-escalation\"><strong>Practical Tips to Prevent Privilege Escalation<\/strong><\/h3>\n\n\n\n<p>This is a big concern, and all of these tips definitely should be in your thought when you are setting up and securing your PostgreSQL servers.<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><strong>Minimize Superusers:<\/strong> Use superuser accounts only for DBA work and never from within the application. The application\u2019s database user should be a non-superuser with limited rights (e.g., no <code>CREATEDB<\/code>, no <code>CREATEROLE<\/code>) (<a href=\"https:\/\/www.postgresql.org\/docs\/current\/role-attributes.html&quot; \\l &quot;:~:text=superuser%20status&quot; \\t &quot;_blank\">PostgreSQL: Documentation: 17: 21.2. Role Attributes<\/a>). This way, even if an attacker gains access to the app\u2019s credentials, they can\u2019t elevate to superuser easily. <\/li>\n\n\n\n<li><strong>Role Auditing:<\/strong> Periodically list your roles and their privileges to ensure no role has more permissions than intended. Check for roles mistakenly left with superuser or roles with broader access than necessary. <\/li>\n\n\n\n<li><strong>Revoke Public Privileges: <\/strong>Revoke default privileges that grant too much to every user For multi-tenant environments, consider using separate schemas or databases and don\u2019t rely on the default public schema for shared use. <\/li>\n\n\n\n<li><strong>Use Security Definer Functions Carefully:<\/strong> Only use <code>SECURITY DEFINER<\/code> when absolutely needed and follow PostgreSQL\u2019s best practices (set a secure <code>search_path<\/code> within the function, and do not allow unfiltered user input to influence privileged actions) Also, the execution of such functions should be limited to trusted roles. <\/li>\n\n\n\n<li><strong>Extension and Maintenance Caution:<\/strong> Be careful which roles can create extensions or use features like <code>COPY TO\/FROM PROGRAM<\/code> (which is superuser-only). If you install procedural languages like PL\/Python or PL\/Perl (untrusted variants), remember they are superuser-only for creation and use \u2013 so avoid enabling them unless required. An untrusted language allows OS-level access, which is another avenue for escalation if a superuser mistakenly executes malicious code. <\/li>\n\n\n\n<li><strong>Monitoring:<\/strong> As with injection, monitor logs for signs of privilege misuse. For example, if you see a role attempting many <code>SET ROLE<\/code> commands or accessing system catalogs it normally wouldn\u2019t, it could be a sign of someone probing for escalation paths. <\/li>\n<\/ul>\n<\/div>\n\n\n<h1 class=\"wp-block-heading\" id=\"h-conclusion\">Conclusion<\/h1>\n\n\n\n<p>PostgreSQL is a secure database by design, but it relies on us engineers and DBAs to configure and use it securely. By following the best practices outlined \u2013 from writing safe SQL in our applications to hardening config files and managing roles wisely \u2013 we significantly reduce the risk of a security incident. Remember, security is not one time activity it\u2019s an ongoing process, keep your PostgreSQL server patched with the latest security update, regularly review configurations as your infrastructure evolves, and stay informed about new vulnerabilities or recommendations from the PostgreSQL community. With a vigilant approach, you can enjoy PostgreSQL\u2019s powerful features while keeping your data safe from injection attacks, misconfiguration leaks, and privilege abuse.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>PostgreSQL is a robust and trusted database platform, but it\u2019s not invincible. The PostgreSQL Global Development Group \u201ctakes security seriously,\u201d enabling users to trust it with mission-critical data (PostgreSQL: Security Information). However, simply running PostgreSQL with default settings or careless practices can be like leaving your solid steel front door unlocked or even opened. You&#8230;&hellip;<\/p>\n","protected":false},"author":342432,"featured_media":106674,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[53,143534],"tags":[158978],"coauthors":[159013],"class_list":["post-106671","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-featured","category-postgresql","tag-postgresql"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/106671","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/342432"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=106671"}],"version-history":[{"count":2,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/106671\/revisions"}],"predecessor-version":[{"id":106677,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/106671\/revisions\/106677"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media\/106674"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=106671"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=106671"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=106671"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=106671"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}