assertWithinInstallDir (src/extension/installer.ts:192) is documented as preventing "symlink/config injection", but it compares resolve()d paths. resolve() does not follow symlinks, so a symlink inside the install directory passes the check while pointing anywhere on disk.
Confirmed end to end. With bin/elastic-demo a symlink to a script outside the install dir:
elastic extension create demo --path <dir>
elastic demo
-> PAYLOAD RAN FROM OUTSIDE THE INSTALL DIR: <dir>/bin/elastic-demo
The registered entrypoint in ~/.elastic/extensions.json reads as an in-directory path, so the registry gives no indication the extension executes something else.
Two separate gaps:
installExtension calls the check, but resolve() makes it bypassable. A cloned github:owner/repo shipping a symlink escapes it.
createLocalExtension never calls the check at all.
Suggested fix: use realpath on both the entrypoint and the install dir before comparing, and call the check on the create path too.
Scope note: the practical attack is narrow. create --path points at a directory the user already controls, and install runs npm install from a cloned repo, which executes arbitrary postinstall code regardless. The reason to fix it is that the function is documented as a security control and does not currently act as one. If that is not worth fixing, the comment should be corrected instead.
Verification caveat: the end-to-end run above used the create --path route. The install github:... variant was verified by unit-testing the resolve() comparison directly, not with a real malicious repository.
Test plan
assertWithinInstallDir(src/extension/installer.ts:192) is documented as preventing "symlink/config injection", but it comparesresolve()d paths.resolve()does not follow symlinks, so a symlink inside the install directory passes the check while pointing anywhere on disk.Confirmed end to end. With
bin/elastic-demoa symlink to a script outside the install dir:The registered
entrypointin~/.elastic/extensions.jsonreads as an in-directory path, so the registry gives no indication the extension executes something else.Two separate gaps:
installExtensioncalls the check, butresolve()makes it bypassable. A clonedgithub:owner/reposhipping a symlink escapes it.createLocalExtensionnever calls the check at all.Suggested fix: use
realpathon both the entrypoint and the install dir before comparing, and call the check on thecreatepath too.Scope note: the practical attack is narrow.
create --pathpoints at a directory the user already controls, andinstallrunsnpm installfrom a cloned repo, which executes arbitrary postinstall code regardless. The reason to fix it is that the function is documented as a security control and does not currently act as one. If that is not worth fixing, the comment should be corrected instead.Verification caveat: the end-to-end run above used the
create --pathroute. Theinstall github:...variant was verified by unit-testing theresolve()comparison directly, not with a real malicious repository.Test plan
extension create --path