skip to Main Content

tldr; script using fs.readFileSync throws EACCESS when called using npm, but not using node

On an ancient (2016) Docker image, I need to run a postinstall NPM script involving Bower (bower install --allow-root), but whenever I do, I get EACCES: permission denied, open '/root/.config/configstore/bower-github.json'. I found out that doing npx bower results in the same. Running npx bower outside of Docker works fine.

Usually, I would easily have dealt with these issues, as they normally arise whenever someone has been executing a command using sudo when they should not have. The fix for those issues is usually to either change the owner back to the current user or just run the bower command with sudo and --allow-root (example 1, example 2).

This, however, is not one of these issues. I am already root!

The full error is like any of the similar issues:

root@eaa32456c249:/var/www/myproj# npx bower --allow-root
/var/www/myproj/node_modules/bower/lib/node_modules/configstore/index.js:54
                throw err;
                ^

Error: EACCES: permission denied, open '/root/.config/configstore/bower-github.json'
You don't have access to this file.

    at Object.openSync (node:fs:585:3)
    at Object.readFileSync (node:fs:453:35)
    at Configstore.get (/var/www/myproj/node_modules/bower/lib/node_modules/configstore/index.js:35:38)
    at new Configstore (/var/www/myproj/node_modules/bower/lib/node_modules/configstore/index.js:28:48)
    at readCachedConfig (/var/www/myproj/node_modules/bower/lib/config.js:19:23)
    at defaultConfig (/var/www/myproj/node_modules/bower/lib/config.js:11:12)
    at Object.<anonymous> (/var/www/myproj/node_modules/bower/lib/index.js:16:32)
    at Module._compile (node:internal/modules/cjs/loader:1101:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
    at Module.load (node:internal/modules/cjs/loader:981:32) {
  errno: -13,
  syscall: 'open',
  code: 'EACCES',
  path: '/root/.config/configstore/bower-github.json'

I cannot elevate my rights any further and adding --allow-root does not do anything. I even inspected the module in question, seeing that the call that always failed was simply this:

readFileSync(this.path, 'utf8');

where this.path was of course '/root/.config/configstore/bower-github.json'.

I then wrote this small test module that does the same, and it ran without issues:

root@eaa32456c249:/var/www/myproj# cat test.js
const execSync = require('child_process').execSync;
const fs = require('fs');
const path = '/root/.config/configstore/bower-github.json';

console.log('exec whoami: ', execSync('whoami').toString());
try {
    const result = execSync('ls -l ' + path, { encoding: 'utf8' });
    console.log('exec ls -l: ', result);
} catch (err) {}

try {
    const parsed = JSON.parse(fs.readFileSync(path, 'utf8', { encoding: 'utf8' }));
    console.log('parsed: ', parsed);
} catch (err) {
    console.error(err.message);
}
root@eaa32456c249:/var/www/myproj# node test.js
exec whoami:  root

exec ls -l:  -rw-r--r-- 1 root root 3 Dec  8 22:55 /root/.config/configstore/bower-github.json

parsed:  {}

A mystery!

2

Answers


  1. Chosen as BEST ANSWER

    tldr; NPM versions 7 and 8 will run as the owner of the root package directory. In other words, if you want to run as root, do chown root.root -R . on the root dir of your project. Earler and later versions lack this behavior, so upgrading NPM will also avoid the issue.


    Update NPM 9 reverts the change, meaning the above just applies to NPM 7 and 8. See PR.


    After letting this simmer for a good while, I just figured I might check who the code was running as, so I opened the module in question (node_modules/configstore/index.js) and added this to the lines preceding the call that failed:

    const execSync = require('child_process').execSync;
    console.log('exec `id`: ', execSync('id', { encoding: 'utf8' }));
    console.log('exec `ls`: ', execSync('ls -l /root', { encoding: 'utf8' }));
    

    Something fishy is indeed going on, as those lines printed:

    exec `id`:  uid=1000 gid=1000 groups=1000
    
    ls: cannot open directory '/root': Permission denied
    

    So somehow, running npx bower as root makes bower run as a user with uid=1000? Running npm run postinstall results in the same issue.

    OK ... let's have a closer look at this. What if I run the bower CLI module manually using node?

    $ node node_modules/.bin/bower  --allow-root
    root@eaa32456c249:/var/www/myproj# node node_modules/.bin/bower  --allow-root
    exec `id`:  uid=0(root) gid=0(root) groups=0(root)
    
    exec `ls`:  total 0
    

    It works - I am still root! So obviously both npx and npm is somehow doing something funky under the covers with regards to who the commands are running as!

    NPM, root and the owner of the CWD

    Digging deep to find the solution

    After my discovery of the above fact, I did some googling and came across this NPM issue, while not actually having a direct explanation set me onto the the trail of Node trying to execute as the owner of the files. And for the first time I actually checked who the owner of the files were:

    root@eaa32456c249:/var/www/myproj# ls -lh node_modules/bower/
    total 72K
    -rw-r--r-- 1 1000 1000  40K Oct 20 08:50 CHANGELOG.md
    -rw-r--r-- 1 1000 1000 1.1K Oct 20 08:50 LICENSE
    -rw-r--r-- 1 1000 1000  14K Oct 20 08:50 README.md
    drwxr-xr-x 2 1000 1000 4.0K Oct 20 08:50 bin
    drwxr-xr-x 9 1000 1000 4.0K Oct 20 08:50 lib
    -rw-r--r-- 1 1000 1000  460 Oct 20 08:50 package.json
    

    Simply doing chown root.root -R node_modules did nothing, so I continued the search. I then read this article, which had this snippet:

    If npm was invoked with root privileges, then it will change the uid to the user account or uid specified by the user config, which defaults to nobody. Set the unsafe-perm flag to run scripts with root privileges.

    OK, let's try setting unsafe-perm to true. No go, I was still running as uid=1000. I then ventured into the actual NPM library on the search for something relevant to uid and found the answer in:

     /usr/local/lib/node_modules/npm/node_modules/@npmcli/promise-spawn/index.js
    

    For NPM 8, it uses this bit of code to determine who to run as:

    const { uid, gid } = isRoot ? inferOwner.sync(cwd) : {}
    

    and as the docs of infer-owner module says:

    Infer the owner of a path based on the owner of its nearest existing parent

    Note the cwd part. It does not look at node_modules, but the current working directory! So I then did chown root.root -R . on the root dir of the project and lo and behold, it worked!

    Addendum

    This behavior is only for NPM versions 7 and 8. NPM versions < 7 (shipped with Node 12-14) work perfectly fine when running as root. So the quickest fix might simply be to use Node 14. Starting with NPM 9 the old behavior is back (probably shipped with Node 20).


  2. This is a very specific solution/scenario – you’re using Docusaurus. This may happen if you set path: '/' in docusaurus.config.js. Set it to something else, maybe you are trying to set routeBasePath and not path.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search