Skip to content

Commit

Permalink
feat: add prefix filtering and loop detection for local images (#83)
Browse files Browse the repository at this point in the history
* feat: add prefix filtering for local images

* fix: prevent infinite loop

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
ascorbic and kodiakhq[bot] authored Oct 5, 2022
1 parent f143ca1 commit f44db69
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 5 deletions.
1 change: 1 addition & 0 deletions example/netlify/functions/ipx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const handler = createIPXHandler({
domains: [
'www.netlify.com'
],
localPrefix: '/img/',
basePath: '/.netlify/builders/ipx/',
responseHeaders: {
'Strict-Transport-Security': 'max-age=31536000',
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"prepack": "yarn build",
"lint": "yarn eslint --ext .ts,.js,.mjs src",
"test": "ava",
"dev": "cd example && netlify dev"
"dev": "netlify dev"
},
"dependencies": {
"@netlify/functions": "^1.3.0",
Expand Down
60 changes: 56 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,68 @@ import etag from 'etag'
import { loadSourceImage } from './http'
import { decodeBase64Params, doPatternsMatchUrl, RemotePattern } from './utils'
export interface IPXHandlerOptions extends Partial<IPXOptions> {
/**
* Path to cache directory
* @default os.tmpdir() /ipx-cache
*/
cacheDir?: string
/**
* Base path for IPX requests
* @default /_ipx/
*/
basePath?: string
propsEncoding?: 'base64' | undefined
/**
* Bypass domain check for remote images
*/
bypassDomainCheck?: boolean
/**
* Restrict local image access to a specific prefix
*/
localPrefix?: string
/**
* Patterns used to verify remote image URLs
*/
remotePatterns?: RemotePattern[]
/**
* Add custom headers to response
*/
responseHeaders?: Record<string, string>
}

const SUBREQUEST_HEADER = 'x-ipx-subrequest'

const plainText = {
'Content-Type': 'text/plain'
}

export function createIPXHandler ({
cacheDir = join(tmpdir(), 'ipx-cache'),
basePath = '/_ipx/',
propsEncoding,
bypassDomainCheck,
remotePatterns,
responseHeaders,
localPrefix,
...opts
}: IPXHandlerOptions = {}) {
const ipx = createIPX({ ...opts, dir: join(cacheDir, 'cache') })
if (!basePath.endsWith('/')) {
basePath = `${basePath}/`
}
if (localPrefix && !localPrefix.startsWith('/')) {
localPrefix = `/${localPrefix}`
}
const handler: Handler = async (event, _context) => {
if (event.headers[SUBREQUEST_HEADER]) {
// eslint-disable-next-line no-console
console.error('Source image loop detected')
return {
statusCode: 400,
body: 'Source image loop detected',
headers: plainText
}
}
let domains = (opts as IPXOptions).domains || []
const remoteURLPatterns = remotePatterns || []
const requestEtag = event.headers['if-none-match']
Expand All @@ -43,18 +83,28 @@ export function createIPXHandler ({
if (params.error) {
return {
statusCode: 400,
body: params.error
body: params.error,
headers: plainText
}
}
id = params.id
modifiers = params.modifiers
}

const requestHeaders: Record<string, string> = {}
const requestHeaders: Record<string, string> = {
[SUBREQUEST_HEADER]: '1'
}
const isLocal = !id.startsWith('http://') && !id.startsWith('https://')
if (isLocal) {
const url = new URL(event.rawUrl)
url.pathname = id
if (localPrefix && !url.pathname.startsWith(localPrefix)) {
return {
statusCode: 400,
body: 'Invalid source image path',
headers: plainText
}
}
id = url.toString()
if (event.headers.cookie) {
requestHeaders.cookie = event.headers.cookie
Expand All @@ -70,7 +120,8 @@ export function createIPXHandler ({
if (!parsedUrl.host) {
return {
statusCode: 403,
body: 'Hostname is missing: ' + id
body: 'Hostname is missing: ' + id,
headers: plainText
}
}

Expand Down Expand Up @@ -107,7 +158,8 @@ export function createIPXHandler ({
`)
return {
statusCode: 403,
body: 'URL not on allowlist: ' + id
body: 'URL not on allowlist: ' + id,
headers: plainText
}
}
}
Expand Down

0 comments on commit f44db69

Please sign in to comment.