Skip to content

Commit

Permalink
feat: add support for CSP
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Oct 26, 2019
1 parent f7b466a commit b99b0d5
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 51 deletions.
14 changes: 14 additions & 0 deletions adonis-typings/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* @adonisjs/shield
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare module '@ioc:Adonis/Core/Response' {
interface ResponseContract {
nonce: string,
}
}
53 changes: 2 additions & 51 deletions adonis-typings/index.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,2 @@
/*
* @adonisjs/shield
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare module '@ioc:Adonis/Addons/Shield' {
export type XFrameOptions = {
enabled: boolean,
action?: 'DENY' | 'SAMEORIGIN',
} | {
enabled: boolean,
action?: 'ALLOW-FROM',
domain: string,
}

// X-Content-Type-Options
export type ContentTypeSniffingOptions = {
enabled: boolean,
}

// HTTP Strict Transport Security (HSTS)
export type HstsOptions = {
enabled: boolean,
maxAge?: string | number,
includeSubDomains?: boolean,
preload?: boolean,
}

// X-XSS-Protection
export type XSSOptions = {
enabled: boolean,
enableOnOldIE?: boolean,
reportUri?: string,
mode?: 'block' | null,
}

// X-Download-Options
export type IENoOpenOptions = {
enabled: boolean,
}

// X-DNS-Prefetch-Control
export type DnsPrefetchOptions = {
enabled: boolean,
allow?: boolean,
}
}
/// <reference path="./shield.ts" />
/// <reference path="./context.ts" />
57 changes: 57 additions & 0 deletions adonis-typings/shield.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* @adonisjs/shield
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare module '@ioc:Adonis/Addons/Shield' {
import { CspOptions as HelmetCspOptions } from 'helmet-csp/dist/lib/types'

export type XFrameOptions = {
enabled: boolean,
action?: 'DENY' | 'SAMEORIGIN',
} | {
enabled: boolean,
action?: 'ALLOW-FROM',
domain: string,
}

// X-Content-Type-Options
export type ContentTypeSniffingOptions = {
enabled: boolean,
}

// HTTP Strict Transport Security (HSTS)
export type HstsOptions = {
enabled: boolean,
maxAge?: string | number,
includeSubDomains?: boolean,
preload?: boolean,
}

// X-XSS-Protection
export type XSSOptions = {
enabled: boolean,
enableOnOldIE?: boolean,
reportUri?: string,
mode?: 'block' | null,
}

// X-Download-Options
export type IENoOpenOptions = {
enabled: boolean,
}

// X-DNS-Prefetch-Control
export type DnsPrefetchOptions = {
enabled: boolean,
allow?: boolean,
}

export type CspOptions = {
enabled: boolean,
} & HelmetCspOptions
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"typescript": "^3.6.3"
},
"dependencies": {
"helmet-csp": "^2.9.4",
"ms": "^2.1.2"
},
"repository": {
Expand Down
17 changes: 17 additions & 0 deletions src/Bindings/Response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* @adonisjs/shield
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import crypto from 'crypto'
import { ResponseConstructorContract } from '@ioc:Adonis/Core/Response'

export default function responseBinding (Response: ResponseConstructorContract) {
Response.getter('nonce', () => {
return crypto.randomBytes(16).toString('hex')
}, true)
}
67 changes: 67 additions & 0 deletions src/csp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* @adonisjs/shield
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

/// <reference path="../adonis-typings/index.ts" />

import helmetCsp from 'helmet-csp'
import { SourceListDirective } from 'helmet-csp/dist/lib/types'

import { CspOptions } from '@ioc:Adonis/Addons/Shield'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { noop } from './noop'

/**
* Reads `nonce` from the ServerResponse and returns appropriate
* string
*/
function nonceFn (_req, res) {
return `'nonce-${res['nonce']}'`
}

/**
* Transform `@nonce` keywords for a given directive
*/
function transformNonceKeywords (directive: SourceListDirective): SourceListDirective {
/**
* Transform array values. There should be only one `@nonce` keyword
*/
if (Array.isArray(directive)) {
const nonceIndex = directive.indexOf('@nonce')
if (nonceIndex > -1) {
directive[nonceIndex] = nonceFn
}
}

return directive
}

/**
* Adds `Content-Security-Policy` header based upon given user options
*/
export function csp (options: CspOptions) {
if (!options.enabled) {
return noop
}

const scriptSrc = options.directives && options.directives.scriptSrc
if (scriptSrc) {
options.directives!.scriptSrc = transformNonceKeywords(options.directives!.scriptSrc!)
}

const styleSrc = options.directives && options.directives.styleSrc
if (styleSrc) {
options.directives!.styleSrc = transformNonceKeywords(options.directives!.styleSrc!)
}

const helmetCspMiddleware = helmetCsp(options)
return function cspMiddlewareFn ({ response }: HttpContextContract) {
response.response['nonce'] = response.nonce
helmetCspMiddleware(response.request, response.response, () => {})
}
}
74 changes: 74 additions & 0 deletions test/csp.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* @adonisjs/shield
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import test from 'japa'
import { HttpContext } from '@adonisjs/http-server/build/standalone'
import { csp } from '../src/csp'

test.group('Csp', () => {
test('return noop function when enabled is false', (assert) => {
const middlewareFn = csp({ enabled: false })
const ctx = HttpContext.create('/', {}, {}, {}, {})
middlewareFn(ctx)

assert.isUndefined(ctx.response.getHeader('Content-Security-Policy'))
})

test('set Content-Security-Policy header', (assert) => {
const middlewareFn = csp({
enabled: true,
directives: {
defaultSrc: [`'self'`],
},
})

const ctx = HttpContext.create('/', {}, {}, {}, {})
middlewareFn(ctx)

assert.equal(ctx.response.getHeader('Content-Security-Policy'), `default-src 'self'`)
})

test('transform @nonce keyword on scriptSrc', (assert) => {
const middlewareFn = csp({
enabled: true,
directives: {
defaultSrc: [`'self'`],
scriptSrc: ['@nonce'],
},
})

const ctx = HttpContext.create('/', {}, {}, {}, {})
ctx.response.nonce = '1234'

middlewareFn(ctx)
assert.equal(
ctx.response.getHeader('Content-Security-Policy'),
`default-src 'self'; script-src 'nonce-1234'`,
)
})

test('transform @nonce keyword on styleSrc', (assert) => {
const middlewareFn = csp({
enabled: true,
directives: {
defaultSrc: [`'self'`],
styleSrc: ['@nonce'],
},
})

const ctx = HttpContext.create('/', {}, {}, {}, {})
ctx.response.nonce = '1234'

middlewareFn(ctx)
assert.equal(
ctx.response.getHeader('Content-Security-Policy'),
`default-src 'self'; style-src 'nonce-1234'`,
)
})
})

0 comments on commit b99b0d5

Please sign in to comment.