Skip to content

Commit

Permalink
fix: disable JS engine for front-matter by default to prevent RCE
Browse files Browse the repository at this point in the history
BREAKING CHANGE: If you previously used JS in front-matter, you'll now have to set `--gray-matter-options 'null'` (or `gray_matter_options: undefined`) to overwrite the new default options that disable the JS engine.

Fixes #99.
  • Loading branch information
simonhaenisch committed Sep 24, 2021
1 parent a1044e0 commit a716259
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 12 deletions.
12 changes: 11 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Options:
--marked-options ......... Set custom options for marked (as a JSON string)
--pdf-options ............ Set custom options for the generated PDF (as a JSON string)
--launch-options ......... Set custom launch options for Puppeteer
--gray-matter-options .... Set custom options for gray-matter
--port ................... Set the port to run the http server on
--md-file-encoding ....... Set the file encoding for the markdown file
--stylesheet-encoding .... Set the file encoding for the stylesheet
Expand Down Expand Up @@ -192,6 +193,7 @@ For default and advanced options see the following links. The default highlight.
| `--marked-options` | `'{ "gfm": false }'` |
| `--pdf-options` | `'{ "format": "Letter", "margin": "20mm", "printBackground": true }'` |
| `--launch-options` | `'{ "args": ["--no-sandbox"] }'` |
| `--gray-matter-options` | `null` |
| `--port` | `3000` |
| `--md-file-encoding` | `utf-8`, `windows1252` |
| `--stylesheet-encoding` | `utf-8`, `windows1252` |
Expand All @@ -203,6 +205,8 @@ For default and advanced options see the following links. The default highlight.

The options can also be set with front-matter or a config file (except `--md-file-encoding` can't be set by front-matter). In that case, remove the leading two hyphens (`--`) from the cli argument name and replace the hyphens (`-`) with underscores (`_`). `--stylesheet` and `--body-class` can be passed multiple times (i. e. to create an array). It's possible to set the output path for the PDF as `dest` in the config. If the same config option exists in multiple places, the priority (from low to high) is: defaults, config file, front-matter, cli arguments.

The JS engine for front-matter is disabled by default for security reasons. You can enable it by overwriting the default gray-matter options (`--gray-matter-options null`, or `gray_matter_options: undefined` in the API).

Example front-matter:

```markdown
Expand Down Expand Up @@ -267,10 +271,16 @@ css: |-
---
```

## Security Consideration
## Security Considerations

### Local file server

By default, this tool serves the current working directory via a http server on `localhost` on a relatively random port (or the port you specify), and that server gets shut down when the process exits (or as soon as it is killed). Please be aware that for the duration of the process this server will be accessible on your local network, and therefore all files within the served folder that the process has permission to read. So as a suggestion, maybe don't run this in watch mode in your system's root folder. 😉

### Don't trust markdown content you don't control

If you intend to use this tool to convert user-provided markdown content, please be aware that - as always - you should sanitize it before processing it with `md-to-pdf`.

## Customization/Development

After cloning and linking/installing globally (`npm link`), just run the transpiler in watch mode (`npm start`). Then you can start making changes to the files and Typescript will transpile them on save. NPM 5+ uses symlinks for locally installed global packages, so all changes are reflected immediately without needing to re-install the package (except when there have been changes to required packages, then re-install using `npm i`). This also means that you can just do a `git pull` to get the latest version onto your machine.
Expand Down
1 change: 1 addition & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const cliFlags = arg({
'--html-pdf-options': String,
'--pdf-options': String,
'--launch-options': String,
'--gray-matter-options': String,
'--port': Number,
'--md-file-encoding': String,
'--stylesheet-encoding': String,
Expand Down
16 changes: 16 additions & 0 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { WatchOptions } from 'chokidar';
import { GrayMatterOption } from 'gray-matter';
import { MarkedOptions } from 'marked';
import { resolve } from 'path';
import { FrameAddScriptTagOptions, launch, PDFOptions } from 'puppeteer';
Expand All @@ -24,6 +25,14 @@ export const defaultConfig: Config = {
},
},
launch_options: {},
gray_matter_options: {
engines: {
js: () =>
new Error(
'The JS engine for front-matter is disabled by default for security reasons. You can enable it by configuring graymatter_options.',
),
},
},
md_file_encoding: 'utf-8',
stylesheet_encoding: 'utf-8',
as_html: false,
Expand Down Expand Up @@ -122,6 +131,13 @@ interface BasicConfig {
*/
launch_options: PuppeteerLaunchOptions;

/**
* Options for gray-matter (front-matter parser).
*
* @see https://github.com/jonschlinkert/gray-matter#options
*/
gray_matter_options: GrayMatterOption<string, any>;

/**
* Markdown file encoding. Default: `utf-8`.
*/
Expand Down
1 change: 1 addition & 0 deletions src/lib/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const helpText = `
--marked-options ${chalk.dim('.........')} Set custom options for marked (as a JSON string)
--pdf-options ${chalk.dim('............')} Set custom options for the generated PDF (as a JSON string)
--launch-options ${chalk.dim('.........')} Set custom launch options for Puppeteer
--gray-matter-options ${chalk.dim('....')} Set custom options for gray-matter
--port ${chalk.dim('...................')} Set the port to run the http server on
--md-file-encoding ${chalk.dim('.......')} Set the file encoding for the markdown file
--stylesheet-encoding ${chalk.dim('....')} Set the file encoding for the stylesheet
Expand Down
19 changes: 13 additions & 6 deletions src/lib/md-to-pdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,21 @@ export const convertMdToPdf = async (
? input.content
: await readFile(input.path, args['--md-file-encoding'] ?? config.md_file_encoding);

const { content: md, data: frontMatterConfig } = grayMatter(mdFileContent);
const { content: md, data: frontMatterConfig } = grayMatter(
mdFileContent,
args['--gray-matter-options'] ? JSON.parse(args['--gray-matter-options']) : config.gray_matter_options,
);

// merge front-matter config
config = {
...config,
...(frontMatterConfig as Config),
pdf_options: { ...config.pdf_options, ...frontMatterConfig.pdf_options },
};
if (frontMatterConfig instanceof Error) {
console.warn('Warning: the front-matter was ignored because it could not be parsed:\n', frontMatterConfig);
} else {
config = {
...config,
...(frontMatterConfig as Config),
pdf_options: { ...config.pdf_options, ...frontMatterConfig.pdf_options },
};
}

const { headerTemplate, footerTemplate, displayHeaderFooter } = config.pdf_options;

Expand Down
36 changes: 31 additions & 5 deletions src/test/api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ import { basename, resolve } from 'path';
import { getDocument } from 'pdfjs-dist/legacy/build/pdf';
import { mdToPdf } from '..';

const getPdfTextContent = async (content: Buffer) => {
const doc = await getDocument({ data: content }).promise;
const page = await doc.getPage(1);
const textContent = (await page.getTextContent()).items.map(({ str }) => str).join('');

return textContent;
};

before(() => {
const filesToDelete = [resolve(__dirname, 'basic', 'api-test.pdf'), resolve(__dirname, 'basic', 'api-test.html')];

Expand Down Expand Up @@ -62,10 +70,28 @@ test('compile the MathJax test', async (t) => {
t.is(pdf.filename, '');
t.truthy(pdf.content);

const doc = await getDocument({ data: pdf.content }).promise;
const page = await doc.getPage(1);
const text = (await page.getTextContent()).items.map(({ str }) => str).join('');
const textContent = await getPdfTextContent(pdf.content);

t.true(textContent.startsWith('Formulas with MathJax'));
t.true(textContent.includes('a≠0'));
});

test('the JS engine is disabled by default', async (t) => {
const css = '`body::before { display: block; content: "${"i am injected"}"}`'; // eslint-disable-line no-template-curly-in-string

const pdf = await mdToPdf({ content: `---js\n{ css: ${css} }\n---` });

const textContent = await getPdfTextContent(pdf.content);

t.is(textContent, '');
});

test('the JS engine for front-matter can be enabled', async (t) => {
const css = '`body::before { display: block; content: "${"i am injected"}"}`'; // eslint-disable-line no-template-curly-in-string

const pdf = await mdToPdf({ content: `---js\n{ css: ${css} }\n---` }, { gray_matter_options: undefined });

const textContent = await getPdfTextContent(pdf.content);

t.true(text.startsWith('Formulas with MathJax'));
t.true(text.includes('a≠0'));
t.is(textContent, 'i am injected');
});

0 comments on commit a716259

Please sign in to comment.