Back to Blog
critical SEVERITY6 min read

ADMIN_HASH Interpolated Into mysql -e: Hex-Encoding the Seed Insert

A container setup script seeds the first administrator account by splicing the shell variable `$ADMIN_HASH` into a quoted SQL literal passed to `mysql -e`. The fix hex-encodes the value with `xxd -p` and decodes it inside MySQL with `unhex()`. No quote character from the shell can now reach the SQL parser. This is a defence-in-depth hardening, not a demonstrated exploit, but it closes off a channel that could have planted extra administrator rows at install time.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published October 21, 2026•Reviewed October 21, 2026

Answer Summary

The affected code is first-party: the container initialization script that seeds `jol.users` with an `admin` account through `mysql -e "insert into jol.users ... values('admin','$ADMIN_HASH',...)"`. If `$ADMIN_HASH` or the script's environment could be influenced, a quote in the value could end the literal. Because the `mysql` client runs semicolon-separated statements, an attacker could then add statements such as an extra row in `jol.privilege` granting `administrator`. The fix passes the value as a hex string to `unhex('$ADMIN_HASH_HEX')`, so only `[0-9a-f]` characters ever appear inside the SQL literal. There is no versioned release, and the issue is classified as CWE-89 (SQL Injection).

Vulnerability at a Glance

cweCWE-89
fixHex-encode the hash with `xxd -p` and decode it server-side with `unhex()`
riskAn influenced `$ADMIN_HASH` could break out of the SQL string and execute extra statements, such as granting `administrator` to an attacker-chosen user, during database initialization
languageBash / shell (MySQL)
root cause`$ADMIN_HASH` is placed directly inside a single-quoted SQL literal in a double-quoted shell string
vulnerabilitySQL injection via shell variable interpolation into `mysql -e`

When the Installer Is the Attack Surface

Most SQL injection write-ups start at an HTTP endpoint. This one starts in the script that runs once, at container build or first boot, to create the very first administrator.

The script generates a random password and derives a salted hash, $ADMIN_HASH. It then hands that hash to the MySQL command-line client by splicing it straight into a SQL string:

mysql -e "insert into jol.users (user_id,password,nick,reg_time) values('admin','$ADMIN_HASH','admin',now());"

The bash double quotes expand $ADMIN_HASH before mysql ever sees the text. Whatever bytes are in that variable become part of the SQL grammar, so a single quote in the value ends the string literal early.

The next line in the same script grants administrator rights through jol.privilege. That makes this insert the most privilege-sensitive statement the application ever runs.

The PR author is candid that this is hardening rather than a proven exploit. It is still worth studying, because "trusted" shell-to-SQL glue like this is copied between projects constantly.

Affected Versions

Affected not applicable (first-party code); the admin-seeding mysql -e insert in the container setup script prior to this change
Fixed in not applicable (first-party code); fixed by the commit that introduces ADMIN_HASH_HEX and unhex()
Ecosystem not applicable (shell script / MySQL)
CVE / GHSA not assigned
CWE CWE-89: Improper Neutralization of Special Elements used in an SQL Command (SQL Injection)

The Vulnerability Explained

The vulnerable code

ADMIN_PASSWORD=`openssl rand -base64 12`
ADMIN_SALT=`openssl rand -hex 2`
ADMIN_MD5=`printf '%s' "$ADMIN_PASSWORD" | openssl md5 -r | cut -d' ' -f1`
ADMIN_HASH=`{ printf '%s%s' "$ADMIN_MD5" "$ADMIN_SALT" | openssl sha1 -binary; printf '%s' "$ADMIN_SALT"; } | openssl base64 -A`
mysql -e "insert into jol.users (user_id,password,nick,reg_time) values('admin','$ADMIN_HASH','admin',now());"

The problem is in the last line, specifically the fragment '$ADMIN_HASH'. Two quoting layers are stacked here:

  1. Bash double quotes around the whole -e argument, which perform variable expansion.
  2. SQL single quotes around the hash, which MySQL treats as a string literal.

Nothing sits between those layers to escape ', \, or ;. Unlike the separate transaction scripts flagged by the same scanner rule, this line does not even double up single quotes. The mysql client has no ? placeholder syntax for -e, so true parameterization is not available at this call site.

Is it exploitable today?

As currently written, the value is the output of openssl base64 -A. Its alphabet is A–Z a–z 0–9 + / =, so a quote cannot appear and the insert is safe by accident.

That safety depends on three assumptions that hold today but are not enforced anywhere:

  • The pipeline stays the same. Someone might switch the encoding to raw binary (openssl sha1 -binary without the base64 step) or to a different hash format such as bcrypt.
  • ADMIN_HASH is never pre-seeded. A future refactor could let the variable be supplied from the environment, a .env file, or a build argument so operators can choose the admin password.
  • The build environment is honest. In a CI pipeline, whoever controls build args or environment variables could control this value.

What an injection would look like

Suppose ADMIN_HASH becomes externally settable. The mysql client splits -e input on semicolons and runs each statement, so a value like this one:

x','admin',now()),('mallory','x','m',now());insert into jol.privilege(user_id,rightstr) values('mallory','administrator');-- 

would close the admin row and add a second user, mallory. It would then run an extra insert into jol.privilege that grants that user administrator. The trailing -- comments out the original ','admin',now()); remainder.

The result is a backdoor administrator baked into a fresh installation. Because the seeding script runs as a MySQL user with full rights on the jol schema, any statement is possible, including DROP, UPDATE, or GRANT.

Real-world impact

The prerequisite is control over the setup environment, which is much narrower than a remote endpoint. The payoff is a persistent, high-privilege foothold that survives until someone audits the privilege table.

The Fix

The change keeps the mysql -e call but guarantees that the SQL literal can only contain hexadecimal digits:

-mysql -e "insert into jol.users (user_id,password,nick,reg_time) values('admin','$ADMIN_HASH','admin',now());"
+ADMIN_HASH_HEX=`printf '%s' "$ADMIN_HASH" | xxd -p | tr -d '\n'`
+mysql -e "insert into jol.users (user_id,password,nick,reg_time) values('admin',unhex('$ADMIN_HASH_HEX'),'admin',now());"

Each piece has a specific job:

  • printf '%s' "$ADMIN_HASH" emits the value without a trailing newline and without interpreting backslash escapes, which echo might do.
  • xxd -p converts every byte to two characters from [0-9a-f]. The output alphabet is now closed: no quote, semicolon, backslash, or comment marker can survive.
  • tr -d '\n' strips the line breaks that xxd -p inserts every 60 hex characters. Without it, the hex string would be split across lines inside the SQL literal.
  • unhex('...') reverses the encoding inside MySQL, so the stored password column receives exactly the original bytes.

This is the shell equivalent of a bound parameter. The data still travels inside the SQL text, but in a form whose character set cannot change the statement's structure. The stored value is unchanged, so the application's login verification keeps working.

A caveat introduced by the fix

The fix adds a dependency on xxd, which in many distributions ships with vim-common rather than coreutils. If the image lacks it, two things go wrong:

  • ADMIN_HASH_HEX silently becomes an empty string.
  • The insert stores unhex(''), an empty password hash.

Worth adding alongside this change:

[ -n "$ADMIN_HASH_HEX" ] || { echo "hex encoding failed" >&2; exit 1; }

Alternatively, use od -An -tx1 | tr -d ' \n', which is available in nearly every base image.

Key Takeaways

  • mysql -e "...'$VAR'..." gives $VAR the power to write SQL. Bash expands the variable before the client parses anything, and -e executes every semicolon-separated statement.
  • An alphabet that is safe by accident is not a control. $ADMIN_HASH is only quote-free because openssl base64 -A happens to be last in the pipeline. Encoding the value as hex makes that property explicit and permanent.
  • Hex plus unhex() (or a X'...' literal) is the practical "parameterized query" for the mysql CLI. The client offers no placeholders for -e, so restrict the character set instead of escaping.
  • Seeding scripts that write to jol.privilege deserve the same scrutiny as login handlers. An injected row there is a persistent administrator.
  • Hardening fixes can add new failure modes. Here, a missing xxd silently produces an empty admin hash unless the script checks the result.

How Orbis AppSec Detected This

  • Source: the shell variable ADMIN_HASH, built from command substitution and potentially overridable by the script's execution environment.
  • Sink: the mysql -e command-line invocation that executes an insert into jol.users statement built by bash string expansion.
  • Missing control: no escaping, encoding, or character-set restriction was applied to $ADMIN_HASH before embedding it inside a single-quoted SQL literal.
  • CWE: CWE-89, Improper Neutralization of Special Elements used in an SQL Command (SQL Injection).
  • Fix: the hash is hex-encoded with xxd -p and decoded in MySQL with unhex(), so only [0-9a-f] characters reach the SQL parser.

Orbis AppSec detected this vulnerability automatically. Try Orbis AppSec on your repositories to find and fix issues like this.

Conclusion

The admin-seeding insert was one environment variable away from letting a build pipeline mint its own administrator. It was safe only because openssl base64 -A happened to emit a quote-free alphabet.

Replacing '$ADMIN_HASH' with unhex('$ADMIN_HASH_HEX') turns that coincidence into a guarantee, without changing what the application stores or how it verifies logins. Pair the change with a non-empty check on ADMIN_HASH_HEX, or an xxd-free encoder.

Then review every other shell script in the codebase that builds SQL through heredocs or mysql -e, since they share the same pattern and the same blind spot.

Prevention and further reading

Frequently Asked Questions

Could the original `$ADMIN_HASH` value ever contain a single quote as generated?

No. As written, the value is the output of `openssl base64 -A`, so it only contains `A–Z`, `a–z`, `0–9`, `+`, `/` and `=`. The risk only appears if the generation pipeline changes or the variable is overridden by the execution environment, which is why the PR calls this defence-in-depth.

Does storing `unhex('$ADMIN_HASH_HEX')` change what ends up in `jol.users.password`?

No. `unhex()` reverses `xxd -p` byte for byte, and the bytes are the same ASCII base64 characters. The stored password hash is identical, and the application's login check is unaffected.

What happens if `xxd` is not installed in the container image?

The command substitution for `ADMIN_HASH_HEX` yields an empty string. Unless the script runs with `set -e -o pipefail`, the insert silently stores `unhex('')`, an empty password hash, for `admin`. Images that use this fix should either install `xxd` or check that `ADMIN_HASH_HEX` is non-empty before running the insert.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1200

Related Articles

critical

Go ArticleRemove() SQL Injection via fmt.Sprintf IN Clause

The Go backend's `ArticleRemove` function built a SQL `DELETE` statement by joining a caller-supplied slice of article IDs directly into the query string with `fmt.Sprintf`. Because the `aids` values came straight from the frontend with no validation or escaping, an attacker could inject arbitrary SQL into the `IN(...)` clause. The fix replaces string concatenation with a parameterized query using placeholders and a `DB.Exec` argument list.

critical

JdbcSinkFunction.invoke() SQL Injection via Unvalidated Identifiers

The `JdbcSinkFunction.invoke()` method in Lacus's RTC engine built SQL INSERT statements with `String.format()`, placing database name, table name, and column names directly into the query string without validation. Although the actual row values used parameterized `?` placeholders, the identifiers themselves were injectable, letting a crafted sink configuration break out of the intended INSERT statement. The fix adds a strict allowlist regex that rejects any identifier containing characters out

critical

Node.js Auth Query SQL Injection via Group Code Interpolation

A sign-in authorization check built its SQL query by mapping an `authorizedGroups` array into quoted string literals and joining them directly into a template literal, creating a classic SQL injection point in a critical authentication path. The fix replaces every interpolated value — including the previously "typed" parameter — with `?` placeholders bound through a value-builder helper, closing off the injection vector entirely.

critical

Toolforge Database Creation SQL Injection via Unicode Backtick Bypass

A critical SQL injection vulnerability in the database creation routine of a Node.js wiki library allowed attackers to inject arbitrary SQL by exploiting a weak validation check. The fix replaces a character-based block with a strict allowlist pattern, ensuring only valid database identifiers reach the SQL engine.

critical

heatmap.php SQL Injection: $_REQUEST Parameters in Unparameterized

A critical SQL injection vulnerability in the heatmap data retrieval endpoint allowed attackers to execute arbitrary database commands by manipulating coordinate bounds or time range parameters. The vulnerability affected all six user-controlled $_REQUEST parameters passed directly into query construction without parameterization.

critical

qemu-img Image Fetcher Hardened Against Symlink Swaps

A VM provisioning script that downloads a cloud image to a fixed path and then runs `qemu-img info` on it had no check that the path was still a regular file by the time the inspection call ran. An automated CWE-78 scan flagged the pattern as command-injection-adjacent; the fix adds an explicit `os.path.islink()` guard so the script refuses to process a symlink instead of silently inspecting whatever file the link resolves to.