#!/usr/bin/env bash # # fix-wp-permissions.sh — reset file and folder permissions on a WordPress install. # # Defaults (WordPress-recommended): # directories 755 # files 644 # wp-config.php 640 (use --strict for 600) # .htaccess 644 # wp-content/uploads writable by the web server group (775 dirs / 664 files) with --group-write # # Run from a URL (pass options after "bash -s --"): # curl -fsSL https://gist.veru.se/Veruse/fix-wp-permissions/raw/HEAD/fix-wp-permissions.sh | bash -s -- --dry-run # curl -fsSL https://gist.veru.se/Veruse/fix-wp-permissions/raw/HEAD/fix-wp-permissions.sh | sudo bash -s -- -p /var/www/example.com -o deploy -g www-data --group-write # # Or locally: # ./fix-wp-permissions.sh [-p path] [-o owner] [-g group] [--group-write] [--strict] [--dry-run] # # With no -p, the current directory is used. # Everything runs inside main(), called on the last line, so a partially # downloaded script never executes. main() { set -euo pipefail WP_PATH="$PWD" OWNER="" GROUP="" GROUP_WRITE=false STRICT=false DRY_RUN=false usage() { cat <<'USAGE' Usage: fix-wp-permissions.sh [options] -p, --path DIR WordPress root (default: current directory) -o, --owner USER chown files to USER (needs root) -g, --group GROUP chown files to GROUP, e.g. www-data (needs root) --group-write make wp-content/uploads writable by the group (775/664) --strict wp-config.php 600 instead of 640 -n, --dry-run show what would change, change nothing -h, --help show this help From a URL: curl -fsSL https://gist.veru.se/Veruse/fix-wp-permissions/raw/HEAD/fix-wp-permissions.sh | bash -s -- --dry-run USAGE exit "${1:-0}" } while [[ $# -gt 0 ]]; do case "$1" in -p|--path) WP_PATH="${2:-}"; shift 2 ;; -o|--owner) OWNER="${2:-}"; shift 2 ;; -g|--group) GROUP="${2:-}"; shift 2 ;; --group-write) GROUP_WRITE=true; shift ;; --strict) STRICT=true; shift ;; -n|--dry-run) DRY_RUN=true; shift ;; -h|--help) usage 0 ;; *) echo "Unknown option: $1" >&2; usage 1 ;; esac done # --- Sanity checks --------------------------------------------------------- [[ -z "$WP_PATH" ]] && { echo "Error: --path is empty." >&2; usage 1; } WP_PATH="$(cd "$WP_PATH" 2>/dev/null && pwd)" || { echo "Error: path not found." >&2; exit 1; } if [[ "$WP_PATH" == "/" || "$WP_PATH" == "$HOME" && ! -d "$WP_PATH/wp-includes" ]]; then echo "Error: refusing to run on '$WP_PATH'." >&2 exit 1 fi if [[ ! -d "$WP_PATH/wp-includes" || ! -d "$WP_PATH/wp-admin" ]]; then echo "Error: '$WP_PATH' doesn't look like a WordPress root (no wp-includes/ or wp-admin/)." >&2 exit 1 fi # wp-config.php can live in the WP root or one level above it. WP_CONFIG="" if [[ -f "$WP_PATH/wp-config.php" ]]; then WP_CONFIG="$WP_PATH/wp-config.php" elif [[ -f "$WP_PATH/../wp-config.php" ]]; then WP_CONFIG="$(cd "$WP_PATH/.." && pwd)/wp-config.php" fi if [[ -n "$OWNER" || -n "$GROUP" ]] && [[ $EUID -ne 0 ]]; then echo "Error: changing ownership needs root. Re-run with sudo, or drop -o/-g." >&2 exit 1 fi [[ -n "$OWNER" ]] && ! id -u "$OWNER" &>/dev/null && { echo "Error: user '$OWNER' doesn't exist." >&2; exit 1; } [[ -n "$GROUP" ]] && ! getent group "$GROUP" &>/dev/null && { echo "Error: group '$GROUP' doesn't exist." >&2; exit 1; } run() { if $DRY_RUN; then echo "[dry-run] $*"; else "$@"; fi } echo "WordPress root : $WP_PATH" echo "wp-config.php : ${WP_CONFIG:-not found}" echo "Owner:group : ${OWNER:-(unchanged)}:${GROUP:-(unchanged)}" echo "Uploads writable by group: $GROUP_WRITE" $DRY_RUN && echo "DRY RUN — nothing will be changed." echo # --- Ownership ------------------------------------------------------------- if [[ -n "$OWNER" || -n "$GROUP" ]]; then echo "Setting ownership..." run chown -R "${OWNER}${GROUP:+:$GROUP}" "$WP_PATH" [[ -n "$WP_CONFIG" && "$WP_CONFIG" != "$WP_PATH/wp-config.php" ]] && \ run chown "${OWNER}${GROUP:+:$GROUP}" "$WP_CONFIG" fi # --- Base permissions ------------------------------------------------------ # -P: don't follow symlinks; chmod on symlinks is skipped by -type d / -type f. echo "Setting directories to 755..." run find -P "$WP_PATH" -type d -exec chmod 755 {} + echo "Setting files to 644..." run find -P "$WP_PATH" -type f -exec chmod 644 {} + # --- Uploads --------------------------------------------------------------- UPLOADS="$WP_PATH/wp-content/uploads" if $GROUP_WRITE && [[ -d "$UPLOADS" ]]; then echo "Making uploads group-writable (775/664)..." run find -P "$UPLOADS" -type d -exec chmod 775 {} + run find -P "$UPLOADS" -type f -exec chmod 664 {} + # setgid so new uploads inherit the group run find -P "$UPLOADS" -type d -exec chmod g+s {} + fi # --- Sensitive files ------------------------------------------------------- if [[ -n "$WP_CONFIG" ]]; then if $STRICT; then echo "Locking wp-config.php to 600..." run chmod 600 "$WP_CONFIG" else echo "Locking wp-config.php to 640..." run chmod 640 "$WP_CONFIG" fi fi [[ -f "$WP_PATH/.htaccess" ]] && run chmod 644 "$WP_PATH/.htaccess" # Any scripts/binaries that genuinely need +x (e.g. in vendor/bin) can be # re-added here, e.g.: # run find "$WP_PATH/wp-content" -path '*/vendor/bin/*' -type f -exec chmod 755 {} + echo echo "Done." $DRY_RUN || echo "Tip: load the site and test uploads, plugin updates and the media library." } main "$@"