CloudLinux Symlink Protection: Fix 403/500 in cPanel

· 5 min read · 9 views · Getwebup

You create a symlink — maybe to share an uploads folder between two sites, or to point a cache directory somewhere else — and the page behind it starts throwing a 403 Forbidden or a bare 500 Internal Server Error. Nothing else on the account changed. What you've run into is CloudLinux's Symlink Race Condition Protection, a kernel-level security feature that most cPanel users never hear about until it breaks something.

Symptom: What You're Actually Seeing

  • The URL behind a symlinked file or folder returns 403 or 500, but the same content opened through its real, non-symlinked path works fine.
  • A PHP include() or require() that points through a symlink fails with failed to open stream: Permission denied, even though ls -l shows normal-looking permissions.
  • The Apache error log has a line like Symbolic link not allowed or link target not accessible right around the timestamp of the failed request.
  • It usually starts right after you create a new symlink, restore an account from backup, or migrate a site between servers.

On CloudLinux-based cPanel servers — which is most shared, reseller, and a fair number of managed VPS setups — the kernel itself enforces symlink ownership checks, independent of whatever Options line you have in .htaccess. This exists to stop a known class of attack: on a shared server, if account A could create a symlink pointing into account B's files and trick a process into following it, A could read or overwrite B's data. CloudLinux closes that gap by requiring, for every symlink Apache or PHP follows:

  1. The symlink and its target must be owned by the exact same user (same UID).
  2. None of the directories in the path to the target can be group- or world-writable, unless the sticky bit is set on them.
  3. The target file or directory itself must not be world-writable.

Fail any one of those and the kernel refuses to resolve the link for the web server or PHP process — even though root, logged in directly, can follow it without any trouble at all.

What you seeUsual cause
403 Forbidden on the symlinked URLSymlink target is owned by a different cPanel user than the symlink itself
500 Internal Server ErrorTarget lives inside a directory that's group- or world-writable (often left at 777 from an old fix)
open_basedir restriction in effect in PHP logsSymlink ownership passed, but the resolved real path falls outside PHP's open_basedir allow-list

The Fix

1. Confirm it really is symlink protection

Check the Apache error log for the account (WHM > Server Configuration > Terminal, or via SSH):

tail -f /usr/local/apache/logs/error_log | grep -i symb

A hit on "Symbolic link not allowed" confirms it — you're not chasing a permissions typo or a caching issue.

2. Check whether ownership actually matches

stat -c '%U %n' /home/user1/public_html/shared
stat -c '%U %n' /home/user2/public_html/uploads

If the two usernames differ, that's the whole problem.

3. Same-account symlink: fix ownership and permissions

chown -h user1:user1 /home/user1/public_html/shared
find /home/user1/public_html/uploads -type d -exec chmod 755 {} \;
find /home/user1/public_html/uploads -type f -exec chmod 644 {} \;

The -h flag on chown matters — without it you'll change the ownership of whatever the link points to instead of the link itself.

4. Cross-account symlink: don't fight the protection

If the target genuinely belongs to a different cPanel user, CloudLinux will not let this through, by design, and root loosening it defeats the entire point of the feature on a shared server. Instead:

  • Sync the files with a scheduled rsync or cron job instead of linking to them live.
  • Put both sites under the same cPanel account as addon domains, so they share one owner and one home directory.
  • Serve the shared assets from a subdomain under the account that already owns them, and reference it by URL rather than by filesystem path.

5. Rule out open_basedir separately

Even after ownership checks pass, PHP can still block the resolved path. In MultiPHP INI Editor, add the real target directory (not just the symlink path) to open_basedir for that domain, then retest.

6. Retest immediately

curl -I https://example.com/shared/file.jpg

Don't rely on a browser reload — a cached 403 can look identical to a fresh one. curl -I gives you the real status without any caching in the way.

If You Have Root or WHM Access

CloudLinux does let root loosen symlink owner-match checks server-wide or per LVE, but treat that as a last resort reserved for genuinely single-tenant boxes — a VPS running one cPanel account, where there's no second user to protect against. On any shared or reseller server, leave it on. It costs you nothing when everything is one owner, and it's the exact control that stops one hacked account from reaching into another.

Prevention

  • Run chown -h on a symlink the moment you create it — don't leave it to inherit whatever the parent directory had.
  • Never symlink across two different cPanel accounts; sync or restructure instead.
  • Keep directories at 755 and files at 644. Resist the urge to chmod -R 777 a folder to "just make it work" — that's usually what breaks symlink protection a week later.
  • After any account restore, migration, or WHM transfer, re-check ownership on every symlink in the account — pkgacct/restorepkg can shift UIDs even when paths stay identical.
  • Document any symlinks you rely on. They're invisible in a normal file listing at a glance and easy for the next person (or future you) to break without realizing it.

Questions people actually ask