Independent comparison · no paid rankings
Home / Blog / Technical / File permissions: fix access without opening the entire application

File permissions: fix access without opening the entire application

An upload folder chmod 777 "so it works" becomes a webshell entry six months later. Permissions need surgery, not blanket fixes.

Hébergeurs.eu Editorial Team 4 min read

Ticket: Permission denied on storage/logs. Quick fix: chmod -R 777 storage. App works. Three months later a .php in storage/uploads runs because nginx still served that tree.

Web permissions are not Unix bureaucracy: separation of code, data, uploads, and execution.

Code: human/CI owner, web read-only. Data (storage, cache): targeted www-data write. Uploads: subtree without interpreter.

Table owner/mode by path — app read-only, storage writable, uploads noexec.

umask and systemd

umask 027 or 002 by group model. PHP-FPM per-app pools.

Avoid deploy scripts as root without final chown.

nginx / Apache

Deny PHP execution under uploads even if .php lands there.

Separate vhost user on constrained shared hosting.

ACL for edge cases

setfacl without opening world. Reset ACL after backup restore — often forgotten.

Automated audit

Post-deploy fail on world-writable find. Alert new SUID. Document temporary exceptions with expiry.

Express permissions audit


find /var/www -type f -perm -0002

find /var/www -type d -perm -0002

find /var/www -name '*.php' -path '*/uploads/*'

Fix: chown deploy:www-data, chmod 640 files / 750 dirs, nginx deny PHP uploads.

Document exceptions with ticket and expiry date. Re-scan post-deploy CI.

Dev training: "permission denied" → strace/id/groups before chmod.

One world-writable upload directory enough for small CMS supply chain compromise.

Incident response

If webshell suspected: find newest modified php under uploads, fix permissions + nginx deny + rotate secrets — chmod alone insufficient.

SELinux/AppArmor on hardened distros: chcon sometimes required after deploy — document contexts, not just chmod.

Containers: volume mount UID mapping — mysterious permission denied often host UID ≠ container user.

Permissions technical debt

Audit legacy: old vendor chmod 777 storage documented nowhere. Remediation sprint plan: nginx deny + ACL + chown.

PHP-FPM pool user per client multi-tenant on custom shared — Unix user isolation > world chmod.

Git does not track permissions by default — post-checkout hook restore modes if needed.

Operational summary

Correct permissions on web hosting separate read, write, and execute by role: deploy writes code, www-data writes uploads and cache, nobody executes PHP in uploads. Shared hosting sometimes compensates with open_basedir; VPS requires explicit discipline.

Post-webshell incident audit: chmod alone insufficient — nginx deny execution, secret rotation, upload vector analysis. Integrate permission scan in CI deploy. Training: permission denied → owner/group before mode.

Step-by-step guide

  1. Identify PHP-FPM user (ps aux | grep php-fpm).
  2. List current web root owner (namei -l /var/www/app/public).
  3. Apply deploy:www-data 750/640 on code.
  4. storage and uploads 770/750 www-data.
  5. nginx deny PHP in uploads.
  6. CI scan world-writable.
  7. Document exceptions.

On malicious upload incident: isolate vhost, preserve file for analysis, rotate DB password if SQL inject suspected, restore permissions from IaC not manual chmod panic.

Shared hosting reality

You cannot always split deploy and www-data users on cheap shared plans. Compensate with open_basedir, disable dangerous PHP functions, and host-level malware scanner — permissions alone insufficient.

When migrating VPS from shared, re-audit every path — assumptions from panel environment do not transfer.

Keep a dated runbook, before/after metrics, post-incident review — cumulative discipline beats Friday night panic.

Keep a dated runbook, before/after metrics, post-incident review — cumulative discipline beats Friday night panic.

Keep a dated runbook, before/after metrics, post-incident review — cumulative discipline beats Friday night panic.

Keep a dated runbook, before/after metrics, post-incident review — cumulative discipline beats Friday night panic.

Keep a dated runbook, before/after metrics, post-incident review — cumulative discipline beats Friday night panic.

Keep a dated runbook, before/after metrics, post-incident review — cumulative discipline beats Friday night panic.

CMS

WP non-exec uploads, Laravel storage writable only. Symlink deploy permissions on current.

Keep a dated runbook, before/after metrics, post-incident review — cumulative discipline beats Friday night panic.

Decide and move forward without blind spots

  1. Identify PHP-FPM usernamei -l on web root, owner deploy:www-data.
  2. Apply 750/640 on code — storage and uploads 770/750, nginx deny PHP in uploads.
  3. Scan world-writable — find in CI post-deploy, exceptions ticketed with expiry date.
  4. Train devs — permission denied → strace/id/groups before panic chmod 777.
  5. Restore from IaC after malicious upload — no manual emergency chmod.

Shared vs VPS: compare limits via our directory and security guides.

Frequently asked questions

755 or 775 for web?

755 on static files if www-data reads alone. 775 only if multiple group users write — strict group, never world-writable.

Owner www-data or deploy?

App files: deploy:www-data 640, dirs 750. Uploads: www-data writable, PHP execution disabled in upload dir via nginx.

ACL vs recursive chmod?

setfacl for precise deploy + www-data cases. chmod -R 777 hides real user/group conflict.

How to audit?

find world-writable, unexpected SUID, check upload paths. Add to CI or post-deploy script.


Next permission denied: change owner before mode — in that order, always.

Compare European hosts

Filter by compliance, location and use case — then open the sheets to verify the real scope.

Browse the directory
Blog

Related reading

All articles →