Skip to content

Commit 3f30c1f

Browse files
authored
Merge pull request #200 from EngineScript/copilot/fix-suffix-length-validation
fix: harden vhost database credential generation and eliminate duplicate validation
2 parents 8221044 + 5b18358 commit 3f30c1f

2 files changed

Lines changed: 27 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ Changes are organized by date, with the most recent changes listed first.
66

77
## 2026-04-12
88

9+
### 🔒 VHOST INSTALL DATABASE CREDENTIAL VALIDATION IMPROVEMENTS
10+
11+
- Added domain hash (`sha256sum`, first 8 hex chars) to `db_name_suffix` in `vhost-install.sh` to prevent silent database name collisions when long domain names are truncated to fit MariaDB's 64-character identifier limit. Added explicit regex validation of the hash output (`^[0-9a-f]{8}$`) to catch pipeline failures with a clear error message.
12+
- Replaced the overly-broad suffix length guard (`>= 64`) with an exact-length check (`!= 14`) that precisely validates the expected suffix format: `_<8-char-hash>_<RAND_CHAR4>`.
13+
- Added backtick rejection to `validate_db_identifier()` as defense-in-depth against SQL injection via backtick-quoted identifiers, guarding against future regex changes.
14+
- Tightened `database_user` validation regex from `^[A-Za-z0-9_]+$` to `^[A-Za-z0-9]+$` to accurately reflect the `RAND_CHAR16` source charset (`a-zA-Z0-9`, no underscores).
15+
- Removed redundant single-quote and backslash sub-checks from `database_password` validation; these are already excluded by the `^[A-Za-z0-9_]+$` regex and were unnecessarily duplicated.
16+
- Removed duplicate post-`source` validations of `${DB}` and `${PSWD}`; both values are fully validated pre-write before the credentials file is created, eliminating double validation.
17+
918
### 🐛 VHOST INSTALL SHELL CORRECTNESS & SECURITY FIXES
1019

1120
- Removed invalid `local` keyword from `create_db_sql` declaration in `scripts/functions/vhost/vhost-install.sh`; `local` has no effect outside a function and was misleading.

scripts/functions/vhost/vhost-install.sh

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ MULTIPART_PUBLIC_SUFFIXES=(
4646
validate_db_identifier() {
4747
local db_identifier="$1"
4848
local domain_context="$2"
49-
if [[ -z "${db_identifier}" || ! "${db_identifier}" =~ ^[A-Za-z][A-Za-z0-9_]*$ ]]; then
49+
# Explicitly reject backticks as defense-in-depth for backtick-quoted SQL identifiers.
50+
if [[ -z "${db_identifier}" || ! "${db_identifier}" =~ ^[A-Za-z][A-Za-z0-9_]*$ || "${db_identifier}" == *'`'* ]]; then
5051
echo "Error: Invalid database name '${db_identifier}' for domain '${domain_context}'." >&2
5152
exit 1
5253
fi
@@ -224,10 +225,18 @@ if [[ "${INSTALL_WORDPRESS}" == "1" ]]; then
224225
# RAND_CHAR4, RAND_CHAR16, and RAND_CHAR32 are random strings (length 4/16/32)
225226
# sourced from /usr/local/bin/enginescript/enginescript-variables.txt.
226227
# Enforce MySQL/MariaDB identifier max length (64 chars) before concatenation.
227-
db_name_suffix="_${RAND_CHAR4}"
228+
# Include a stable hash of the full domain to avoid collisions when truncation occurs.
229+
domain_hash="$(printf '%s' "${DOMAIN}" | sha256sum | awk '{print $1}' | cut -c1-8)"
230+
if [[ ! "${domain_hash}" =~ ^[0-9a-f]{8}$ ]]; then
231+
echo "Error: Failed to generate domain hash for database name suffix (got '${domain_hash}')." >&2
232+
exit 1
233+
fi
234+
db_name_suffix="_${domain_hash}_${RAND_CHAR4}"
228235
max_db_name_len=64
229-
if (( ${#db_name_suffix} >= max_db_name_len )); then
230-
echo "Error: Invalid random suffix length for database name generation." >&2
236+
# Expected: '_' (1) + domain_hash (8) + '_' (1) + RAND_CHAR4 (4) = 14 chars.
237+
expected_db_name_suffix_len=14
238+
if (( ${#db_name_suffix} != expected_db_name_suffix_len )); then
239+
echo "Error: Invalid random suffix length for database name generation (expected ${expected_db_name_suffix_len}, got ${#db_name_suffix})." >&2
231240
exit 1
232241
fi
233242
max_domain_without_tld_len=$((max_db_name_len - ${#db_name_suffix}))
@@ -245,13 +254,14 @@ if [[ "${INSTALL_WORDPRESS}" == "1" ]]; then
245254
credentials_dir="/home/EngineScript/mysql-credentials"
246255
credentials_file="${credentials_dir}/${DOMAIN}.txt"
247256
# Ensure parent directory exists and is restricted before writing sensitive data
248-
# Validate generated credentials before writing any sensitive data to disk
249-
if [[ -z "${database_user}" || ${#database_user} -lt 8 || ${#database_user} -gt 80 || ! "${database_user}" =~ ^[A-Za-z0-9_]+$ ]]; then
250-
echo "Error: Invalid generated MariaDB user '${database_user}' for domain '${DOMAIN}' (must be 8-80 characters and contain only letters, numbers, or underscores)." >&2
257+
# Validate generated credentials before writing any sensitive data to disk.
258+
# RAND_CHAR16 uses a-zA-Z0-9 only; regex matches that exact charset.
259+
if [[ -z "${database_user}" || ${#database_user} -lt 8 || ${#database_user} -gt 80 || ! "${database_user}" =~ ^[A-Za-z0-9]+$ ]]; then
260+
echo "Error: Invalid generated MariaDB user '${database_user}' for domain '${DOMAIN}' (must be 8-80 characters and contain only letters or numbers)." >&2
251261
exit 1
252262
fi
253263

254-
if [[ -z "${database_password}" || ! "${database_password}" =~ ^[A-Za-z0-9_]+$ || "${database_password}" == *"'"* || "${database_password}" == *"\\"* ]]; then
264+
if [[ -z "${database_password}" || ! "${database_password}" =~ ^[A-Za-z0-9_]+$ ]]; then
255265
echo "Error: Invalid generated database password for domain '${DOMAIN}'." >&2
256266
exit 1
257267
fi
@@ -267,20 +277,6 @@ if [[ "${INSTALL_WORDPRESS}" == "1" ]]; then
267277

268278
source "${credentials_file}"
269279

270-
# Validate DB identifier before interpolating into SQL
271-
if [[ -z "${DB}" || ! "${DB}" =~ ^[A-Za-z][A-Za-z0-9_]*$ ]]; then
272-
echo "Error: Invalid database name '${DB}' for domain '${DOMAIN}'." >&2
273-
exit 1
274-
fi
275-
276-
# Validate DB password before interpolating into SQL single-quoted string.
277-
# Allow printable ASCII generally, but reject characters that would break
278-
# single-quoted SQL interpolation without escaping (' and \).
279-
if [[ -z "${PSWD}" || ! "${PSWD}" =~ ^[[:print:]]+$ || "${PSWD}" == *"'"* || "${PSWD}" == *"\\"* ]]; then
280-
echo "Error: Invalid database password for domain '${DOMAIN}'." >&2
281-
exit 1
282-
fi
283-
284280
echo "Randomly generated MySQL database credentials for ${DOMAIN}."
285281

286282
printf -v create_db_sql "CREATE DATABASE \`%s\` CHARACTER SET utf8mb4 COLLATE utf8mb4_uca1400_ai_ci;" "${DB}"

0 commit comments

Comments
 (0)