| 1 | #!/usr/bin/env bash |
| 2 | # |
| 3 | # fix-wp-permissions.sh — reset file and folder permissions on a WordPress install. |
| 4 | # |
| 5 | # Defaults (WordPress-recommended): |
| 6 | # directories 755 |
| 7 | # files 644 |
| 8 | # wp-config.php 640 (use --strict for 600) |
| 9 | # .htaccess 644 |
| 10 | # wp-content/uploads writable by the web server group (775 dirs / 664 files) with --group-write |
| 11 | # |
| 12 | # Run from a URL (pass options after "bash -s --"): |
| 13 | # curl -fsSL https://gist.veru.se/Veruse/fix-wp-permissions/raw/HEAD/fix-wp-permissions.sh | bash -s -- --dry-run |
| 14 | # 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 |
| 15 | # |
| 16 | # Or locally: |
| 17 | # ./fix-wp-permissions.sh [-p path] [-o owner] [-g group] [--group-write] [--strict] [--dry-run] |
| 18 | # |
| 19 | # With no -p, the current directory is used. |
| 20 | |
| 21 | # Everything runs inside main(), called on the last line, so a partially |
| 22 | # downloaded script never executes. |
| 23 | main() { |
| 24 | set -euo pipefail |
| 25 | |
| 26 | WP_PATH="$PWD" |
| 27 | OWNER="" |
| 28 | GROUP="" |
| 29 | GROUP_WRITE=false |
| 30 | STRICT=false |
| 31 | DRY_RUN=false |
| 32 | |
| 33 | usage() { |
| 34 | cat <<'USAGE' |
| 35 | Usage: fix-wp-permissions.sh [options] |
| 36 | |
| 37 | -p, --path DIR WordPress root (default: current directory) |
| 38 | -o, --owner USER chown files to USER (needs root) |
| 39 | -g, --group GROUP chown files to GROUP, e.g. www-data (needs root) |
| 40 | --group-write make wp-content/uploads writable by the group (775/664) |
| 41 | --strict wp-config.php 600 instead of 640 |
| 42 | -n, --dry-run show what would change, change nothing |
| 43 | -h, --help show this help |
| 44 | |
| 45 | From a URL: |
| 46 | curl -fsSL https://gist.veru.se/Veruse/fix-wp-permissions/raw/HEAD/fix-wp-permissions.sh | bash -s -- --dry-run |
| 47 | USAGE |
| 48 | exit "${1:-0}" |
| 49 | } |
| 50 | |
| 51 | while [[ $# -gt 0 ]]; do |
| 52 | case "$1" in |
| 53 | -p|--path) WP_PATH="${2:-}"; shift 2 ;; |
| 54 | -o|--owner) OWNER="${2:-}"; shift 2 ;; |
| 55 | -g|--group) GROUP="${2:-}"; shift 2 ;; |
| 56 | --group-write) GROUP_WRITE=true; shift ;; |
| 57 | --strict) STRICT=true; shift ;; |
| 58 | -n|--dry-run) DRY_RUN=true; shift ;; |
| 59 | -h|--help) usage 0 ;; |
| 60 | *) echo "Unknown option: $1" >&2; usage 1 ;; |
| 61 | esac |
| 62 | done |
| 63 | |
| 64 | # --- Sanity checks --------------------------------------------------------- |
| 65 | |
| 66 | [[ -z "$WP_PATH" ]] && { echo "Error: --path is empty." >&2; usage 1; } |
| 67 | WP_PATH="$(cd "$WP_PATH" 2>/dev/null && pwd)" || { echo "Error: path not found." >&2; exit 1; } |
| 68 | |
| 69 | if [[ "$WP_PATH" == "/" || "$WP_PATH" == "$HOME" && ! -d "$WP_PATH/wp-includes" ]]; then |
| 70 | echo "Error: refusing to run on '$WP_PATH'." >&2 |
| 71 | exit 1 |
| 72 | fi |
| 73 | |
| 74 | if [[ ! -d "$WP_PATH/wp-includes" || ! -d "$WP_PATH/wp-admin" ]]; then |
| 75 | echo "Error: '$WP_PATH' doesn't look like a WordPress root (no wp-includes/ or wp-admin/)." >&2 |
| 76 | exit 1 |
| 77 | fi |
| 78 | |
| 79 | # wp-config.php can live in the WP root or one level above it. |
| 80 | WP_CONFIG="" |
| 81 | if [[ -f "$WP_PATH/wp-config.php" ]]; then WP_CONFIG="$WP_PATH/wp-config.php" |
| 82 | elif [[ -f "$WP_PATH/../wp-config.php" ]]; then WP_CONFIG="$(cd "$WP_PATH/.." && pwd)/wp-config.php" |
| 83 | fi |
| 84 | |
| 85 | if [[ -n "$OWNER" || -n "$GROUP" ]] && [[ $EUID -ne 0 ]]; then |
| 86 | echo "Error: changing ownership needs root. Re-run with sudo, or drop -o/-g." >&2 |
| 87 | exit 1 |
| 88 | fi |
| 89 | [[ -n "$OWNER" ]] && ! id -u "$OWNER" &>/dev/null && { echo "Error: user '$OWNER' doesn't exist." >&2; exit 1; } |
| 90 | [[ -n "$GROUP" ]] && ! getent group "$GROUP" &>/dev/null && { echo "Error: group '$GROUP' doesn't exist." >&2; exit 1; } |
| 91 | |
| 92 | run() { |
| 93 | if $DRY_RUN; then echo "[dry-run] $*"; else "$@"; fi |
| 94 | } |
| 95 | |
| 96 | echo "WordPress root : $WP_PATH" |
| 97 | echo "wp-config.php : ${WP_CONFIG:-not found}" |
| 98 | echo "Owner:group : ${OWNER:-(unchanged)}:${GROUP:-(unchanged)}" |
| 99 | echo "Uploads writable by group: $GROUP_WRITE" |
| 100 | $DRY_RUN && echo "DRY RUN — nothing will be changed." |
| 101 | echo |
| 102 | |
| 103 | # --- Ownership ------------------------------------------------------------- |
| 104 | |
| 105 | if [[ -n "$OWNER" || -n "$GROUP" ]]; then |
| 106 | echo "Setting ownership..." |
| 107 | run chown -R "${OWNER}${GROUP:+:$GROUP}" "$WP_PATH" |
| 108 | [[ -n "$WP_CONFIG" && "$WP_CONFIG" != "$WP_PATH/wp-config.php" ]] && \ |
| 109 | run chown "${OWNER}${GROUP:+:$GROUP}" "$WP_CONFIG" |
| 110 | fi |
| 111 | |
| 112 | # --- Base permissions ------------------------------------------------------ |
| 113 | # -P: don't follow symlinks; chmod on symlinks is skipped by -type d / -type f. |
| 114 | |
| 115 | echo "Setting directories to 755..." |
| 116 | run find -P "$WP_PATH" -type d -exec chmod 755 {} + |
| 117 | |
| 118 | echo "Setting files to 644..." |
| 119 | run find -P "$WP_PATH" -type f -exec chmod 644 {} + |
| 120 | |
| 121 | # --- Uploads --------------------------------------------------------------- |
| 122 | |
| 123 | UPLOADS="$WP_PATH/wp-content/uploads" |
| 124 | if $GROUP_WRITE && [[ -d "$UPLOADS" ]]; then |
| 125 | echo "Making uploads group-writable (775/664)..." |
| 126 | run find -P "$UPLOADS" -type d -exec chmod 775 {} + |
| 127 | run find -P "$UPLOADS" -type f -exec chmod 664 {} + |
| 128 | # setgid so new uploads inherit the group |
| 129 | run find -P "$UPLOADS" -type d -exec chmod g+s {} + |
| 130 | fi |
| 131 | |
| 132 | # --- Sensitive files ------------------------------------------------------- |
| 133 | |
| 134 | if [[ -n "$WP_CONFIG" ]]; then |
| 135 | if $STRICT; then |
| 136 | echo "Locking wp-config.php to 600..." |
| 137 | run chmod 600 "$WP_CONFIG" |
| 138 | else |
| 139 | echo "Locking wp-config.php to 640..." |
| 140 | run chmod 640 "$WP_CONFIG" |
| 141 | fi |
| 142 | fi |
| 143 | |
| 144 | [[ -f "$WP_PATH/.htaccess" ]] && run chmod 644 "$WP_PATH/.htaccess" |
| 145 | |
| 146 | # Any scripts/binaries that genuinely need +x (e.g. in vendor/bin) can be |
| 147 | # re-added here, e.g.: |
| 148 | # run find "$WP_PATH/wp-content" -path '*/vendor/bin/*' -type f -exec chmod 755 {} + |
| 149 | |
| 150 | echo |
| 151 | echo "Done." |
| 152 | $DRY_RUN || echo "Tip: load the site and test uploads, plugin updates and the media library." |
| 153 | } |
| 154 | |
| 155 | main "$@" |
Veruse / fix-wp-permissions.sh
Last active 3 weeks ago
bash <(curl -fsSL https://gist.veru.se/Veruse/fix-wp-permissions/raw/HEAD/fix-wp-permissions.sh)
Revision 9e65b5e93f20699d48cfa098d7e63c486c40c868