npm/cli

[BUG] root permissions do not carry forward into execution

ReillyBova opened this issue · 14 comments

Current Behavior:

I am using webpack to bind a dev server to the privileged port 443 on an M1 Macbook Air (Apple Silicon) running the latest version of Big Sur. In order to bind to the privileged port, my npm start script must be run with root permissions, so I execute sudo npm start. Unfortunately, the root permissions do not carry into the execution, and my server fails to bind to port 443 and throws an error that is identical to the error thrown when run without sudo:

✖ 「wds」:  Error: listen EACCES: permission denied 127.0.0.1:443
    at Server.setupListenHandle [as _listen2] (net.js:1301:21)
    at listenInCluster (net.js:1366:12)
    at GetAddrInfoReqWrap.doListen [as callback] (net.js:1503:7)
    at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:69:8) {

Expected Behavior:

I expect sudo npm run to have no issues binding to port 443 because it has root permissions. This is how it behaves when I backdate npm to 6.14.12.

Steps To Reproduce:

  1. Create an npm start script that tries to bind to a privileged port (e.g. 443)
  2. Run npm start with root permissions (e.g. sudo npm start if on a Mac)
  3. You should see an error
  4. Try binding to the same port directly in your terminal using root permissions
  5. You should not see an error
  6. Redo steps 1-3 using npm v6. Confirm no error is thrown.

I'm not sure how broad this issue is. Here are some open questions:

  1. Is this just related to binding to privileged ports?
  2. Is this just an M1 Apple Silicon problem?
  3. Is this just a Mac OS 11 problem?
  4. Is this an issue on all Mac OS versions?
  5. Is this an issue on all devices?
  6. Etc.

Environment:

  • OS macOS Big Sur 11.2.3
  • Node: 14.16.1
  • npm: 7.10.0

Maybe due to the change below:

The user, group, uid, gid, and unsafe-perms configurations are no longer relevant. When npm is run as root, scripts are always run with the effective uid and gid of the working directory owner.

described in v7.0.0-beta.0 CHANGELOG


Perhaps because now when running script in root, v7.x will use infer-owner to get the uid and gid based on cwd. It may change the original root uid and gid (0).

const st = fs.lstatSync(path)
threw = false
const { uid, gid } = st
cache.set(path, { uid, gid })
return { uid, gid }

Then pass them to the spawn function as options.

const promiseSpawn = (cmd, args, opts, extra = {}) => {
const cwd = opts.cwd || process.cwd()
const isRoot = process.getuid && process.getuid() === 0
const { uid, gid } = isRoot ? inferOwner.sync(cwd) : {}
return promiseSpawnUid(cmd, args, {
...opts,
cwd,
uid,
gid
}, extra)
}

@ReillyBova as @alienzhou noted, this was an intended change in v7 - you can resolve this by running your scripts/npm within a root owned folder

@ReillyBova as @alienzhou noted, this was an intended change in v7 - you can resolve this by running your scripts/npm within a root owned folder

I'm unclear on why this was an intended change in v7. Running local npm as root and chown'ing the project folder to root seems like a rather messy way to deal with this. I wouldn't consider that a resolution, it's just sidestepping the problem and creating new problems. This issue is really wreaking havoc.

This has caused pretty significant problems with us too - we've had to switch away from npm-scripts to Makefiles to avoid this. This violates conventions of other tools (e.g. make) and is just generally confusing. I've yet to actually see an explanation of why this change was introduced - can a maintainer at least help us understand that?

So this is the source of my debugging nightmare... I've lost hours trying to figure out why my node application could not access a file in a privileged location, when I do sudo npm start. It wasn't clear what was going on until until I printed os.userInfo(), and even after that I had to search quite a bit to find this issue.

A warning message for this new behavior would be very appreciated.

To be frank, it's a little irresponsible not to provide more transparency here. This is a deeply non-obvious behavior. If there are good security reasons for this, then please make them known.

https://docs.npmjs.com/cli/v7/using-npm/scripts#user

When npm is run as root, scripts are always run with the effective uid and gid of the working directory owner.

Hit this issue in CI

  • jenkins checks out the repo as jenkins user (uid=1000)
  • docker container runs npm run as root

🤦

you can resolve this by running your scripts/npm within a root owned folder

This is not an option as this will break other programs.

If anyone is looking for a workaround, you can use yarn instead.

While someone could agree on the choice to infer uid ang gid from folder there will still be someone who wants to override this, I have two programs in the same folder and I need to start one with root and the other with non-root user and I haven't any workaround here. An option to override this breaking change would be the best IMO

Does anyone know a viable workaround to this issue? Changing ownership of a directory where source lives in not viable for my project.
Thanks!

If anyone is looking for a workaround, you can use yarn instead.

Yarn exhibits exactly the same problem.

Any updates on that?
I serve my web application on port 443 and I need use sudo for that. This behavior breaks applications' start-up.

Error: listen EACCES: permission denied 127.0.0.1:443
    at Server.setupListenHandle [as _listen2] (net.js:1303:21)
    at listenInCluster (net.js:1368:12)
    at GetAddrInfoReqWrap.doListen [as callback] (net.js:1505:7)
    at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:71:8) {
  code: 'EACCES',
  errno: -13,
  syscall: 'listen',
  address: '127.0.0.1',
  port: 443
}

Same problem is described here: #4589

Does anyone know a viable workaround to this issue? Changing ownership of a directory where source lives in not viable for my project.

The only workaround is to not use npm to start your script. Just use node directly

Yarn does work though...

-> % ls -l
total 12
-rw-r--r-- 1 mike mike  54 Apr 26 12:13 index.js
-rw-r--r-- 1 mike mike 230 Apr 26 12:12 package.json
-rw-r--r-- 1 mike mike 275 Apr 26 12:17 yarn.lock

-> % cat package.json 
{
  "name": "blah",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

-> % cat index.js 
const os = require('os');
console.log(os.userInfo());
-> % yarn --version
3.2.0
-> % yarn start
{
  uid: 1000,
  gid: 1000,
  username: 'mike',
  homedir: '/home/mike',
  shell: '/bin/zsh'
}
-> % sudo yarn start
{
  uid: 0,
  gid: 0,
  username: 'root',
  homedir: '/root',
  shell: '/bin/bash'
}

-> % ls -ld
drwxr-xr-x 3 mike mike 4096 Apr 26 12:17 .

also worked for yarn version 1.22.18 when I tried that.

But yeah maybe @robertsLando is right, using node directly would be the most simple thing. Still, npm scripts can get pretty complicated, so I wonder if this is always possible without a lot of work.

I have created #4811 please go to upvote it so maybe it get noticed by maintainers