Skip to content

Commit

Permalink
Implement security restriction options to allow limiting demo app
Browse files Browse the repository at this point in the history
  • Loading branch information
kimmobrunfeldt committed Apr 9, 2020
1 parent 4b15a12 commit 0400fa0
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 7 deletions.
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,12 @@ and requests are direct connections to it.

## Examples

*Note: the demo Heroku app runs on a free dyno which sleep after idle.
A request to sleeping dyno may take even 30 seconds.*
**⚠️ Restrictions ⚠️:**

* For security reasons the urls have been restricted and HTML rendering is disabled. For full demo, run this app locally or deploy to Heroku.
* The demo Heroku app runs on a free dyno which sleep after idle. A request to sleeping dyno may take even 30 seconds.



**The most minimal example, render google.com**

Expand Down Expand Up @@ -106,14 +110,18 @@ https://url-to-pdf-api.herokuapp.com/api/render?url=http://google.com&waitFor=in

**Render HTML sent in JSON body**

*NOTE: Demo app has disabled html rendering for security reasons.*

```bash
curl -o html.pdf -XPOST -d'{"html": "<body>test</body>"}' -H"content-type: application/json" https://url-to-pdf-api.herokuapp.com/api/render
curl -o html.pdf -XPOST -d'{"html": "<body>test</body>"}' -H"content-type: application/json" http://localhost:9000/api/render
```

**Render HTML sent as text body**

*NOTE: Demo app has disabled html rendering for security reasons.*

```bash
curl -o html.pdf -XPOST -d@page.html -H"content-type: text/html" https://url-to-pdf-api.herokuapp.com/api/render
curl -o html.pdf -XPOST -d@test/resources/large.html -H"content-type: text/html" http://localhost:9000/api/render
```

## API
Expand Down Expand Up @@ -264,11 +272,11 @@ The only required parameter is `url`.
**Example:**

```bash
curl -o google.pdf -XPOST -d'{"url": "http://google.com"}' -H"content-type: application/json" https://url-to-pdf-api.herokuapp.com/api/render
curl -o google.pdf -XPOST -d'{"url": "http://google.com"}' -H"content-type: application/json" http://localhost:9000/api/render
```

```bash
curl -o html.pdf -XPOST -d'{"html": "<body>test</body>"}' -H"content-type: application/json" https://url-to-pdf-api.herokuapp.com/api/render
curl -o html.pdf -XPOST -d'{"html": "<body>test</body>"}' -H"content-type: application/json" http://localhost:9000/api/render
```

### POST /api/render - (HTML)
Expand All @@ -283,7 +291,7 @@ paremeter.

```bash
curl -o receipt.html https://rawgit.com/wildbit/postmark-templates/master/templates_inlined/receipt.html
curl -o html.pdf -XPOST [email protected] -H"content-type: text/html" https://url-to-pdf-api.herokuapp.com/api/render?pdf.scale=1
curl -o html.pdf -XPOST [email protected] -H"content-type: text/html" http://localhost:9000/api/render?pdf.scale=1
```

## Development
Expand Down
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"joi": "^11.1.1",
"lodash": "^4.17.15",
"morgan": "^1.9.1",
"normalize-url": "^5.0.0",
"pdf-parse": "^1.1.1",
"puppeteer": "^2.0.0",
"server-destroy": "^1.0.1",
Expand Down
8 changes: 8 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ function createApp() {
logger.info('ALLOW_HTTP=true, unsafe requests are allowed. Don\'t use this in production.');
}

if (config.ALLOW_URLS) {
logger.info(`ALLOW_URLS set! Allowed urls patterns are: ${config.ALLOW_URLS.join(' ')}`);
}

if (config.DISABLE_HTML_INPUT) {
logger.info('DISABLE_HTML_INPUT=true! Input HTML is disabled!');
}

const corsOpts = {
origin: config.CORS_ORIGIN,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'HEAD', 'PATCH'],
Expand Down
6 changes: 6 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,20 @@ const config = {
LOG_LEVEL: process.env.LOG_LEVEL,
ALLOW_HTTP: process.env.ALLOW_HTTP === 'true',
DEBUG_MODE: process.env.DEBUG_MODE === 'true',
DISABLE_HTML_INPUT: process.env.DISABLE_HTML_INPUT === 'true',
CORS_ORIGIN: process.env.CORS_ORIGIN || '*',
BROWSER_WS_ENDPOINT: process.env.BROWSER_WS_ENDPOINT,
BROWSER_EXECUTABLE_PATH: process.env.BROWSER_EXECUTABLE_PATH,
API_TOKENS: [],
ALLOW_URLS: [],
};

if (process.env.API_TOKENS) {
config.API_TOKENS = process.env.API_TOKENS.split(',');
}

if (process.env.ALLOW_URLS) {
config.ALLOW_URLS = process.env.ALLOW_URLS.split(',');
}

module.exports = config;
71 changes: 71 additions & 0 deletions src/http/render-http.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
const { URL } = require('url');
const _ = require('lodash');
const normalizeUrl = require('normalize-url');
const ex = require('../util/express');
const renderCore = require('../core/render-core');
const logger = require('../util/logger')(__filename);
const config = require('../config');

function getMimeType(opts) {
if (opts.output === 'pdf') {
Expand All @@ -19,6 +23,8 @@ function getMimeType(opts) {

const getRender = ex.createRoute((req, res) => {
const opts = getOptsFromQuery(req.query);

assertOptionsAllowed(opts);
return renderCore.render(opts)
.then((data) => {
if (opts.attachmentName) {
Expand Down Expand Up @@ -53,6 +59,7 @@ const postRender = ex.createRoute((req, res) => {
opts.html = req.body;
}

assertOptionsAllowed(opts);
return renderCore.render(opts)
.then((data) => {
if (opts.attachmentName) {
Expand All @@ -63,6 +70,70 @@ const postRender = ex.createRoute((req, res) => {
});
});

function isHostMatch(host1, host2) {
return {
match: host1.toLowerCase() === host2.toLowerCase(),
type: 'host',
part1: host1.toLowerCase(),
part2: host2.toLowerCase(),
};
}

function isRegexMatch(urlPattern, inputUrl) {
const re = new RegExp(`${urlPattern}`);

return {
match: re.test(inputUrl),
type: 'regex',
part1: inputUrl,
part2: urlPattern,
};
}

function isNormalizedMatch(url1, url2) {
return {
match: normalizeUrl(url1) === normalizeUrl(url2),
type: 'normalized url',
part1: url1,
part2: url2,
};
}

function isUrlAllowed(inputUrl) {
const urlParts = new URL(inputUrl);

const matchInfos = _.map(config.ALLOW_URLS, (urlPattern) => {
if (_.startsWith(urlPattern, 'host:')) {
return isHostMatch(urlPattern.split(':')[1], urlParts.host);
} else if (_.startsWith(urlPattern, 'regex:')) {
return isRegexMatch(urlPattern.split(':')[1], inputUrl);
}

return isNormalizedMatch(urlPattern, inputUrl);
});

const isAllowed = _.some(matchInfos, info => info.match);
if (!isAllowed) {
logger.info('The url was not allowed because:');
_.forEach(matchInfos, (info) => {
logger.info(`${info.part1} !== ${info.part2} (with ${info.type} matching)`);
});
}

return isAllowed;
}

function assertOptionsAllowed(opts) {
const isDisallowedHtmlInput = !_.isString(opts.url) && config.DISABLE_HTML_INPUT;
if (isDisallowedHtmlInput) {
ex.throwStatus(403, 'Rendering HTML input is disabled.');
}

if (_.isString(opts.url) && config.ALLOW_URLS.length > 0 && !isUrlAllowed(opts.url)) {
ex.throwStatus(403, 'Url not allowed.');
}
}

function getOptsFromQuery(query) {
const opts = {
url: query.url,
Expand Down

0 comments on commit 0400fa0

Please sign in to comment.