{"record":{"id":"bfda9d0fbeadc4b4","repo":"apache/cordova-android","slug":"file-src-is-located-outside-the-plugin-direct","errorCode":null,"errorMessage":"File \"${src}\" is located outside the plugin directory \"${plugin_dir}\"","messagePattern":"File \"(.+?)\" is located outside the plugin directory \"(.+?)\"","errorType":"validation","errorClass":"CordovaError","httpStatus":null,"severity":"error","filePath":"lib/pluginHandlers.js","lineNumber":215,"sourceCode":"    events.emit('verbose', '<' + type + '> is not supported for android plugins');\n};\n\nmodule.exports.getUninstaller = function (type) {\n    if (handlers[type] && handlers[type].uninstall) {\n        return handlers[type].uninstall;\n    }\n\n    events.emit('verbose', '<' + type + '> is not supported for android plugins');\n};\n\nfunction copyFile (plugin_dir, src, project_dir, dest, link) {\n    src = path.resolve(plugin_dir, src);\n    if (!fs.existsSync(src)) throw new CordovaError('\"' + src + '\" not found!');\n\n    // check that src path is inside plugin directory\n    const real_path = fs.realpathSync(src);\n    const real_plugin_path = fs.realpathSync(plugin_dir);\n    if (!isPathInside(real_path, real_plugin_path)) { throw new CordovaError('File \"' + src + '\" is located outside the plugin directory \"' + plugin_dir + '\"'); }\n\n    dest = path.resolve(project_dir, dest);\n\n    // check that dest path is located in project directory\n    if (!isPathInside(dest, project_dir)) { throw new CordovaError('Destination \"' + dest + '\" for source file \"' + src + '\" is located outside the project'); }\n\n    fs.mkdirSync(path.dirname(dest), { recursive: true });\n    if (link) {\n        symlinkFileOrDirTree(src, dest);\n    } else {\n        fs.cpSync(src, dest, { recursive: true });\n    }\n}\n\n// Same as copy file but throws error if target exists\nfunction copyNewFile (plugin_dir, src, project_dir, dest, link) {\n    const target_path = path.resolve(project_dir, dest);\n    if (fs.existsSync(target_path)) { throw new CordovaError('\"' + target_path + '\" already exists!'); }","sourceCodeStart":197,"sourceCodeEnd":233,"githubUrl":"https://github.com/apache/cordova-android/blob/7c1e190064e349ffa4bbc6ac37b77cd773e4dbd3/lib/pluginHandlers.js#L197-L233","documentation":"A security check in copyFile: it resolves both the source file and the plugin directory through fs.realpathSync (following symlinks) and requires the file to be inside the plugin directory via isPathInside. It throws when a plugin tries to copy a file that really lives outside its own tree, blocking path-traversal-style plugin content.","triggerScenarios":"A plugin.xml src containing ../ escapes (e.g. <resource-file src=\"../../shared/logo.png\"/>), or a file inside the plugin that is a symlink pointing outside (e.g. npm link-ed local plugins, or a symlinked node_modules dependency file). The realpath check sees through the symlink and rejects it.","commonSituations":"Developing plugins with `npm link` or a symlinked plugins/<id> directory during local testing; monorepo plugins referencing shared assets via ../; a repo checked out with symlinked submodules.","solutions":["Move or copy the referenced file physically inside the plugin directory and use a src relative to the plugin root without ../ segments","For local development, remove symlinks: uninstall the plugin, then `cordova plugin add /abs/path/to/plugin` (real path) instead of npm link","If shared code is needed across plugins, publish it as its own plugin and declare it as a <dependency> instead of reaching outside"],"exampleFix":"# before\n<resource-file src=\"../../shared/assets/logo.png\" target=\"res/drawable/logo.png\" />\n\n# after (copy shared/assets/logo.png into the plugin first)\n<resource-file src=\"src/shared/logo.png\" target=\"res/drawable/logo.png\" />","handlingStrategy":"validation","validationCode":"// before install: flag src paths that escape the plugin root\nconst path = require('path');\nconst escapes = srcAttr.split('/').filter(p => p === '..').length > 0;\nif (escapes) throw new Error('plugin.xml src escapes the plugin dir and will be rejected');","typeGuard":null,"tryCatchPattern":"try { await cordova.plugin('add', p); } catch (e) {\n  if (/located outside the plugin directory/.test(e.message)) { /* inline the file into the plugin or use a <dependency> plugin */ }\n}","preventionTips":["Never reference files outside the plugin directory from plugin.xml","Avoid npm link for local plugin testing; add plugins by real path","Audit third-party plugin.xml for ../ paths before installing"],"tags":["cordova","cordova-android","plugin-xml","security","path-traversal","symlink"],"backgroundTag":"path-traversal-blocked","analyzedSha":"7c1e190064e349ffa4bbc6ac37b77cd773e4dbd3","analyzedAt":"2026-08-22T04:57:58.868Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}