Node-html-pdf: npm audit vulnerability

Created on 18 Sep 2019  路  21Comments  路  Source: marcbachmann/node-html-pdf

Hey there, an npm advisory is out for this package.
https://www.npmjs.com/advisories/1095

Is there any timeline to resolve this?

Most helpful comment

I decided to replicate potential attacks if possible. In order to do that I played with phantomjs arguments (https://phantomjs.org/api/command-line.html). Below you can find the experiment and accompanying results.

Preparation

  1. Clone the repo,
  2. cd examples,
  3. echo "private content" > private,
  4. Add script adding contents of local file into the businesscard/businesscard.html file (right before the </body> tag):
    <div id="file" style="color: white"></div>
    <script>
        const el = document.getElementById('file');
        function reqListener () {
            el.innerHTML = this.responseText;
        }

        var oReq = new XMLHttpRequest();
        oReq.addEventListener("load", reqListener);
        oReq.open("GET", "{{path}}");
        oReq.send();
    </script>
  1. Pass the path to private file when rendering from template i.e. replace in serve-http/index.js.
const html = tmpl.replace('{{image}}', `file://${require.resolve('../businesscard/image.png')}`);

with

  const html = tmpl
    .replace('{{image}}', `file://${require.resolve('../businesscard/image.png')}`)
    .replace('{{path}}', `file://${require.resolve('../private')}`);

Test

Rendering from web server with default phantomArgs

Start web server:

$ node serve-http/index.js
Listening on http://localhost:8080

Go to http://localhost:8080 in your browser. Look at the output from console.

NETWORK_ERR: XMLHttpRequest Exception 101: A network error occurred in synchronous requests.

  undefined:107 in send

=> Cross-origin request was not allowed. Private content not visible in the rendered pdf.

Rendering from web server with web security turned off (--web-security=no)

Update serve-http/index.js line 9 to:

pdf.create(html, {width: '50mm', height: '90mm', phantomArgs: ['--web-security=no']}).toStream((err, stream) => {

Start web server:

$ node serve-http/index.js
Listening on http://localhost:8080

Go to http://localhost:8080 in your browser and observe private content in response.
=> Cross-origin request was allowed. Private content visible in the rendered pdf.

Rendering from web server with web security turned off and access to local files turned off (--web-security=no --local-url-access=false)

Update serve-http/index.js line 9 to:

pdf.create(html, {width: '50mm', height: '90mm', phantomArgs: ['--web-security=no', '--local-url-access=false']}).toStream((err, stream) => {

Start web server:

$ node serve-http/index.js
Listening on http://localhost:8080

=> Request for local file was not allowed. Private content not visible in the rendered pdf. However, since the image was loaded via FS url it's not loaded either.

Rendering from CLI with default phantomArgs

Replace ../bin/index.js line 25 with:

  var html = fs
    .readFileSync(source, 'utf8')
    .replace('{{image}}', `file://${require.resolve('../examples/businesscard/image.png')}`)
    .replace('{{path}}', `file://${require.resolve('../examples/private')}`);

Run:

$ ../bin/index.js businesscard/businesscard.html test.pdf

=> Request for local file was allowed. Private content visible in the rendered pdf.

Rendering from CLI with access to local files turned off (--local-url-access=false)

Replace ../bin/index.js line 31 with:

  base: 'file://' + path.resolve(source), phantomArgs: ['--local-url-access=false'],

Run:

$ ../bin/index.js businesscard/businesscard.html test.pdf

Request for local file was not allowed. Private content not visible in the rendered pdf.

Conclusions

According to the above tests looks like suggestion from @marcbachmann to use --local-url-access=false phantomjs argumnet succesfully prevents loading files from local file system using XHR requests. However, I'm not sure if this is the only attack vector NPM researchers had in mind, so it is not, in any way, a confirmation the vulnerability does not exist.

All 21 comments

Thanks for great plugin. Do you have any plan fix it sooner? thank you

Seems puppeteer will be the answer. Not much other well supported choices for html to pdf converters.

Any alternatives for this packages apart from the puppeteer approach ?

@278kunal There is json to pdf. https://www.npmjs.com/package/pdfkit which is very popular. You would have to convert your existing templates from html to json but it works.

I'm not sure why you can't just provide the phantomjs argument using the config
phantomArgs: ['--local-url-access=false']

@marcbachmann Could you explain how this would help with the npm vulnerability above? https://www.npmjs.com/advisories/1095

I decided to replicate potential attacks if possible. In order to do that I played with phantomjs arguments (https://phantomjs.org/api/command-line.html). Below you can find the experiment and accompanying results.

Preparation

  1. Clone the repo,
  2. cd examples,
  3. echo "private content" > private,
  4. Add script adding contents of local file into the businesscard/businesscard.html file (right before the </body> tag):
    <div id="file" style="color: white"></div>
    <script>
        const el = document.getElementById('file');
        function reqListener () {
            el.innerHTML = this.responseText;
        }

        var oReq = new XMLHttpRequest();
        oReq.addEventListener("load", reqListener);
        oReq.open("GET", "{{path}}");
        oReq.send();
    </script>
  1. Pass the path to private file when rendering from template i.e. replace in serve-http/index.js.
const html = tmpl.replace('{{image}}', `file://${require.resolve('../businesscard/image.png')}`);

with

  const html = tmpl
    .replace('{{image}}', `file://${require.resolve('../businesscard/image.png')}`)
    .replace('{{path}}', `file://${require.resolve('../private')}`);

Test

Rendering from web server with default phantomArgs

Start web server:

$ node serve-http/index.js
Listening on http://localhost:8080

Go to http://localhost:8080 in your browser. Look at the output from console.

NETWORK_ERR: XMLHttpRequest Exception 101: A network error occurred in synchronous requests.

  undefined:107 in send

=> Cross-origin request was not allowed. Private content not visible in the rendered pdf.

Rendering from web server with web security turned off (--web-security=no)

Update serve-http/index.js line 9 to:

pdf.create(html, {width: '50mm', height: '90mm', phantomArgs: ['--web-security=no']}).toStream((err, stream) => {

Start web server:

$ node serve-http/index.js
Listening on http://localhost:8080

Go to http://localhost:8080 in your browser and observe private content in response.
=> Cross-origin request was allowed. Private content visible in the rendered pdf.

Rendering from web server with web security turned off and access to local files turned off (--web-security=no --local-url-access=false)

Update serve-http/index.js line 9 to:

pdf.create(html, {width: '50mm', height: '90mm', phantomArgs: ['--web-security=no', '--local-url-access=false']}).toStream((err, stream) => {

Start web server:

$ node serve-http/index.js
Listening on http://localhost:8080

=> Request for local file was not allowed. Private content not visible in the rendered pdf. However, since the image was loaded via FS url it's not loaded either.

Rendering from CLI with default phantomArgs

Replace ../bin/index.js line 25 with:

  var html = fs
    .readFileSync(source, 'utf8')
    .replace('{{image}}', `file://${require.resolve('../examples/businesscard/image.png')}`)
    .replace('{{path}}', `file://${require.resolve('../examples/private')}`);

Run:

$ ../bin/index.js businesscard/businesscard.html test.pdf

=> Request for local file was allowed. Private content visible in the rendered pdf.

Rendering from CLI with access to local files turned off (--local-url-access=false)

Replace ../bin/index.js line 31 with:

  base: 'file://' + path.resolve(source), phantomArgs: ['--local-url-access=false'],

Run:

$ ../bin/index.js businesscard/businesscard.html test.pdf

Request for local file was not allowed. Private content not visible in the rendered pdf.

Conclusions

According to the above tests looks like suggestion from @marcbachmann to use --local-url-access=false phantomjs argumnet succesfully prevents loading files from local file system using XHR requests. However, I'm not sure if this is the only attack vector NPM researchers had in mind, so it is not, in any way, a confirmation the vulnerability does not exist.

Hi @sin6pi7 the last post was really informative, thanks for putting this together. I figure that suggestion from @marcbachmann would prevent such an attack. However what I am wondering is, given that I, and I suspect other people also, have npm audit apart of their CI/CD pipeline. This deployment step is currently failing because npm audit fails.

Do you guys turn off npm audit or not have it apart of your CI/CD pipeline? @sin6pi7 @marcbachmann ? What do you generally do?

Thanks again for your time.

Thanks @antoniovassell, appreciated 馃憤

I figure that suggestion from @marcbachmann would prevent such an attack.

Please remember, that this only tackles the XHR for local files reported in the original description on npm website - might be that npm researchers found other attack vectors, which I have not covered. Suggestion for disabling local files access in phantomjs should be evaluated against your own use case.

Do you guys turn of npm audit or not have it apart of your CI/CD pipeline? @sin6pi7 @marcbachmann ? What do you generally do?

Yeah, I've been there and decided to use https://www.npmjs.com/package/npm-audit-resolver - it allows you to choose whether you would like to ignore something if you're making an informed decision.

Hi @sin6pi7 , npm-audit-resolver looks like a great option for that. Thanks for the insights.

Sadly, we don't have the privilege of choosing alternatives to npm audit in our CI. Is there a way to mitigate this other than choosing other libraries? Or is there an alternative that only requires minimal changes to the templates initially processed by this library?

Thanks :)

In your package.json file, replace "html-pdf": "^2.2.0" with

"html-pdf": "git+https://github.com/418sec/node-html-pdf.git"

You can try this until a new patch would be published, that repo seems to be safe to use imo.

In your package.json file, replace "html-pdf": "^2.2.0" with

"html-pdf": "git+https://github.com/418sec/node-html-pdf.git"

You can try this until a new patch would be published, that repo seems to be safe to use imo.

Can we get this published on NPM? That would be great. Love what you did there.

Can we get this published on NPM? That would be great. Love what you did there.

It's not my repo, I just found it, @snowmac
Ask the owners maybe?

@418sec can you publish to NPM?

Can @marcbachmann merge and publish the change? If not maybe we need to fork the repo and rename it so we can publish it.

I have the fix in #616, just phantomjs fails to run on the ci. It's working locally.

Thank you @marcbachmann

There is a fix in 3.0.1 now but the advisory still lists it as affected
https://www.npmjs.com/advisories/1095

I contacted NPM support and this advisory 1095 is now fixed in the 3.0.1 version https://www.npmjs.com/advisories/1095/versions

@antoniovassell can this be closed now?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

vivekiyer114 picture vivekiyer114  路  5Comments

adman020 picture adman020  路  5Comments

Messilimeng picture Messilimeng  路  4Comments

cmoulliard picture cmoulliard  路  3Comments

tashikomaaa picture tashikomaaa  路  5Comments