Skip to content

Request path containing invalid UTF-8 bytes returns 500 (Postgres SQLSTATE[22021]) instead of 404 #2657

Description

@phubner

Shlink version

5.0.2

PHP version

8.4.20

How do you serve Shlink

Docker image

Database engine

PostgreSQL

Database version

18.1

Current behavior

A GET request whose path contains bytes that are not valid UTF-8 makes Shlink pass
the raw, undecoded bytes to Postgres as the short-code query parameter. Postgres
rejects the parameter and Shlink returns a 500:

Shlink.ERROR - PDOException: SQLSTATE[22021]: Character not in repertoire: 7
  ERROR:  invalid byte sequence for encoding "UTF8": 0xc0
  CONTEXT:  unnamed portal parameter $1
  in /etc/shlink/vendor/doctrine/dbal/src/Driver/PDO/Exception.php:24
#0 .../doctrine/dbal/src/Driver/PDO/Statement.php(57)

Also reproduced on 5.1.6 (PHP 8.5.7) and on Postgres 16.14 — same result on every
combination.

This is not only scanner noise. We see two traffic sources in production:

  1. Vulnerability scanners probing paths like /%C0.
  2. Real users: some in-app browsers (Facebook/Instagram, fbclid present)
    occasionally append a mangled invisible Unicode character to shared short
    links; the bytes arrive invalid and the visitor gets a 500 instead of the
    not-found redirect. Each hit also fires 5xx-based alerting, indistinguishable
    from a genuine outage.

NOTE: I did use claude to help reproduce and diagnose the issue. I wanted to make sure everything was fully tested before writing any tickets.

Expected behavior

Since short codes are drawn from an ASCII alphabet, a path containing invalid
UTF-8 can never match a short URL — the expected response is a 404 (or the
configured not-found redirect), not a 500. Validating/decoding the short-code
path segment as UTF-8 (or restricting it to the short-code alphabet) before it
reaches the DB layer would short-circuit these to the regular not-found handling.

Minimum steps to reproduce

Self-contained docker-compose (official images only, no cloud components):

services:
  shlink:
    image: shlinkio/shlink:5.1.6
    ports: ["8090:8080"]
    environment:
      DEFAULT_DOMAIN: "localhost:8090"
      IS_HTTPS_ENABLED: "false"
      DB_DRIVER: postgres
      DB_HOST: db
      DB_NAME: shlink
      DB_USER: shlink
      DB_PASSWORD: shlink
    depends_on: { db: { condition: service_healthy } }
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: shlink
      POSTGRES_USER: shlink
      POSTGRES_PASSWORD: shlink
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U shlink"]
      interval: 2s
      timeout: 3s
      retries: 20
docker compose up -d
curl -i 'http://localhost:8090/%C0'
# → HTTP 500; shlink log shows:
#   SQLSTATE[22021] ... invalid byte sequence for encoding "UTF8": 0xc0

Any invalid byte works (%FF, %C0, or raw bytes). Valid multi-byte UTF-8
(e.g. /%E2%81%A3) is handled fine — only undecodable sequences trigger it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    • Status
      No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions