{"action":"created","issue":{"url":"https://api.github.com/repos/expressjs/serve-static/issues/114","repository_url":"https://api.github.com/repos/expressjs/serve-static","labels_url":"https://api.github.com/repos/expressjs/serve-static/issues/114/labels{/name}","comments_url":"https://api.github.com/repos/expressjs/serve-static/issues/114/comments","events_url":"https://api.github.com/repos/expressjs/serve-static/issues/114/events","html_url":"https://github.com/expressjs/serve-static/issues/114","id":352247509,"node_id":"MDU6SXNzdWUzNTIyNDc1MDk=","number":114,"title":"Option to deny or ignore symlinks for extra security","user":{"login":"kachkaev","id":608862,"node_id":"MDQ6VXNlcjYwODg2Mg==","avatar_url":"https://avatars3.githubusercontent.com/u/608862?v=4","gravatar_id":"","url":"https://api.github.com/users/kachkaev","html_url":"https://github.com/kachkaev","followers_url":"https://api.github.com/users/kachkaev/followers","following_url":"https://api.github.com/users/kachkaev/following{/other_user}","gists_url":"https://api.github.com/users/kachkaev/gists{/gist_id}","starred_url":"https://api.github.com/users/kachkaev/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kachkaev/subscriptions","organizations_url":"https://api.github.com/users/kachkaev/orgs","repos_url":"https://api.github.com/users/kachkaev/repos","events_url":"https://api.github.com/users/kachkaev/events{/privacy}","received_events_url":"https://api.github.com/users/kachkaev/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":0,"created_at":"2018-08-20T18:31:44Z","updated_at":"2018-08-20T18:34:12Z","closed_at":null,"author_association":"NONE","body":"It seems that `express.static` follows symlinks by default, which may become dangerous depending on how the static folder gets populated. If I understand things right, an attacker may read a sensitive file such as as a private SSH key in some situations because of this.\r\n\r\nI can imagine a couple of scenarios:\r\n\r\n1. An app user is given an option to upload an archive with their static files and this archive contains symlinks to something like `/home/ubuntu/.ssh/id_rsa`.\r\n\r\n1. A static folder served by express contains a build artifact from the CI/CD pipeline that has been just hacked. An attacker cannot access the we server, but can modify new artifacts as get build and this allows them to access sensitive files on the web server.\r\n\r\nIt'd be great if serving symlinks could be disabled as simple as this is done for `dotfiles`, e.g.:\r\n\r\n```js\r\nvar serve = serveStatic('public/ftp', { dotfiles: 'ignore', symlinks: 'ignore' });\r\n```\r\n\r\nMy current workaround:\r\n\r\n```js\r\nconst notASymlink = async (pathname) => {\r\n  try {\r\n    const stat = await fs.lstat(pathname);\r\n    return !stat.isSymbolicLink();\r\n  } catch (e) {\r\n    return false;\r\n  }\r\n};\r\n\r\n// this simulates { symlinks: \"ignore\" }\r\nconst statics = express.static(staticRootDir, { dotfiles: \"ignore\" });\r\nserver.get(\"*\", async (req, res, next) => {\r\n  if (await notASymlink(path.resolve(staticRootDir, req.path.substring(1)))) {\r\n    return statics(req, res, next);\r\n  }\r\n  return next();\r\n});\r\n```\r\n\r\nIt may also be possible to [memoize](https://lodash.com/docs/4.17.10#memoize) the results of `notASymlink` function, but such caching needs to be done with caution because a regular file can become a harmful symlink at any time. \r\n\r\n```js\r\nconst { memoize } = require('lodash');\r\n\r\nconst staticFolderLastUpdated = new Date().getTime();\r\nconst notASymlinkUsingCache = memoize(\r\n  notASymlink,\r\n  (path) => `${staticFolderLastUpdated}|${path}`,\r\n);\r\n```\r\n\r\nI'm not sure if this trick gives enough performance gain though – I did not run any benchmarks.\r\n\r\n--- \r\n\r\nI may be too paranoic, but I believe that `symlinks` could be set to `ignore` or `deny` is some future major version to protect people from a risk they may not be aware of."},"comment":{"url":"https://api.github.com/repos/expressjs/serve-static/issues/comments/414418358","html_url":"https://github.com/expressjs/serve-static/issues/114#issuecomment-414418358","issue_url":"https://api.github.com/repos/expressjs/serve-static/issues/114","id":414418358,"node_id":"MDEyOklzc3VlQ29tbWVudDQxNDQxODM1OA==","user":{"login":"dougwilson","id":67512,"node_id":"MDQ6VXNlcjY3NTEy","avatar_url":"https://avatars3.githubusercontent.com/u/67512?v=4","gravatar_id":"","url":"https://api.github.com/users/dougwilson","html_url":"https://github.com/dougwilson","followers_url":"https://api.github.com/users/dougwilson/followers","following_url":"https://api.github.com/users/dougwilson/following{/other_user}","gists_url":"https://api.github.com/users/dougwilson/gists{/gist_id}","starred_url":"https://api.github.com/users/dougwilson/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dougwilson/subscriptions","organizations_url":"https://api.github.com/users/dougwilson/orgs","repos_url":"https://api.github.com/users/dougwilson/repos","events_url":"https://api.github.com/users/dougwilson/events{/privacy}","received_events_url":"https://api.github.com/users/dougwilson/received_events","type":"User","site_admin":false},"created_at":"2018-08-20T18:34:12Z","updated_at":"2018-08-20T18:34:12Z","author_association":"MEMBER","body":"To clarify: are you reporting this as a vulnerability or as a feature request?"}}
{"actor":{"display_login":"dougwilson"}}