Skip to content

fix: URL-encode and shell-escape sqlx migration DSN#2175

Open
linuxkd wants to merge 1 commit into
hashtopolis:devfrom
linuxkd:fix/migration-dsn-escaping
Open

fix: URL-encode and shell-escape sqlx migration DSN#2175
linuxkd wants to merge 1 commit into
hashtopolis:devfrom
linuxkd:fix/migration-dsn-escaping

Conversation

@linuxkd
Copy link
Copy Markdown

@linuxkd linuxkd commented May 28, 2026

Problem

In src/inc/startup/setup.php the sqlx migration DSN is built by string concatenation and passed unquoted and non-URL-encoded straight into exec('/usr/bin/sqlx migrate run ... -D ' . $database_uri).

This breaks server startup whenever the database username or password contains:

  • URL-special characters (@ : / # ? [ ]) — they corrupt the DSN that sqlx parses (e.g. an @ in the password is read as the userinfo/host separator).
  • shell-special characters ($ & ; | < > ( ) *, space, backtick, etc.) — sh -c interprets them before sqlx ever runs (e.g. & backgrounds the truncated command and splits the DSN).

Hit in production with an auto-generated DB password containing $ and &:

Start initialization process...
sh: 1: @<db-host>:3306/<db>: not found
Failed to run migrations:
error: error with configuration: invalid port number
Hashtopolis setup.php failed (exit code 255)

This affects both the mysql and postgres DSNs (same construction).

Fix

Two small, independent hardening changes:

  1. rawurlencode() the userinfo (username + password) components of the DSN so URL-special characters survive parsing.
  2. escapeshellarg() the arguments passed to exec() (the migrations --source path and the -D DSN) so shell-special characters are passed literally.

No behavioral change for credentials that contain only safe characters.

Fixes #2174

The migration DSN in src/inc/startup/setup.php was built by string
concatenation and passed unquoted, non-URL-encoded into exec(). Database
passwords/usernames containing URL-special (@ : / # ? [ ]) or
shell-special ($ & ; | space etc.) characters corrupted the DSN and
broke server startup. rawurlencode() the userinfo and escapeshellarg()
the exec arguments. Affects both the mysql and postgres DSNs.
@linuxkd
Copy link
Copy Markdown
Author

linuxkd commented May 28, 2026

Standalone reproduction + fix verification

Since standing up the full stack is heavy, here is a self-contained PHP script that proves both halves of the bug and the fix, using a password with the exact problem characters: p@ss$w0rd&x:y/z (contains URL-special @ : / and shell-special $ &). Run with the php CLI:

=== INPUT ===
user: dbuser
pass: p@ss$w0rd&x:y/z
host: host  port: 3306  db: hashtopolis

=== BEFORE (broken) ===
DSN: mysql://dbuser:p@ss$w0rd&x:y/z@host:3306/hashtopolis
parse_url() ->
  scheme: (none)
  host:   (none)
  port:   (none)
  user:   (none)
  pass:   (none)
  host correct? NO
  port correct? NO
shell command (unquoted): printf "[%s]\n" sqlx migrate run -D mysql://dbuser:p@ss$w0rd&x:y/z@host:3306/hashtopolis
what the shell actually runs (tokens):
sh: 1: x:y/z@host:3306/hashtopolis: not found
  [sqlx]
  [migrate]
  [run]
  [-D]
  [mysql://dbuser:p@ss]

=== AFTER (fixed) ===
DSN: mysql://dbuser:p%40ss%24w0rd%26x%3Ay%2Fz@host:3306/hashtopolis
parse_url() ->
  scheme: mysql
  host:   host
  port:   3306
  user:   dbuser
  pass:   p@ss$w0rd&x:y/z
  host correct? YES
  port correct? YES
  user roundtrip correct? YES
  pass roundtrip correct? YES
shell command (escaped): printf "[%s]\n" sqlx migrate run -D 'mysql://dbuser:p%40ss%24w0rd%26x%3Ay%2Fz@host:3306/hashtopolis'
what the shell actually runs (tokens):
  [sqlx]
  [migrate]
  [run]
  [-D]
  [mysql://dbuser:p%40ss%24w0rd%26x%3Ay%2Fz@host:3306/hashtopolis]
  DSN arrived as exactly ONE token? YES

=== SUMMARY ===
BEFORE: host/port parse correct = NO (bug reproduced)
AFTER:  host/port/user/pass parse correct = YES (fixed)
AFTER:  DSN is a single shell token        = YES (fixed)

Before: raw concatenation produces a DSN that parse_url() cannot parse at all (host/port/user/pass all empty), and the unquoted & makes sh background the truncated command and try to execute the @host:3306/... remainder — mirroring the production sh: 1: ...: not found + invalid port number symptoms exactly.

After: rawurlencode() on the userinfo makes the DSN parse correctly and round-trip the original password, and escapeshellarg() delivers the DSN to exec() as exactly one shell token with no splitting.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant