fileoverview import { debug as d } from '@hint/utils-debug' ; import { mergeIgnoreIncludeArrays , toLowerCaseArray } from '@hint/utils-string' ; import { includedHeaders , isDataURI , normalizeHeaderValue } from '@hint/utils-network' ; import { HintContext } from 'hint/dist/src/lib/hint-context' ; import { FetchEnd , IHint } from 'hint/dist/src/lib/types' ; import { Severity } from '@hint/utils-types' ; import meta from './meta' ; import { getMessage , MessageName } from './i18n.import' ; const debug = d ( __filename ) ; const serverHeaderContainsTooMuchInformation = ( serverHeaderValue ) : boolean => { const regex = [ /\/?v?\d\.(\d+\.?)*/ , /\(.*\)/ , /(mod_|openssl|php)/ ] ; return regex . some ( ( r ) => { return r . test ( serverHeaderValue ) ; } ) ; } ; interface ISpecialHeadersWithMessage { [ key ] : MessageName ; } export default class NoDisallowedHeadersHint implements IHint { public static readonly meta = meta ; public constructor ( context ) { let generalDisallowedHeaders = [ 'public-key-pins' , 'public-key-pins-report-only' , 'x-aspnet-version' , 'x-aspnetmvc-version' , 'x-powered-by' , 'x-runtime' , 'x-version' ] ; const specialDisallowedHeaders = { expires : 'disallowedExpiresHeader' , host : 'disallowedHostHeader' , p3p : 'disallowedP3PHeader' , pragma : 'disallowedPragmaHeader' , via : 'disallowedViaHeader' , 'x-frame-options' : 'disallowedXFrameOptionsHeader' } ; let includeHeaders ; let ignoreHeaders ; const loadHintConfigs = ( ) => { includeHeaders = ( context . hintOptions && context . hintOptions . include ) || [ ] ; ignoreHeaders = ( context . hintOptions && context . hintOptions . ignore ) || [ ] ; generalDisallowedHeaders = mergeIgnoreIncludeArrays ( generalDisallowedHeaders , ignoreHeaders , includeHeaders ) ; } ; const validateGeneralHeaders = ( { response , resource } ) => { const headers = includedHeaders ( response . headers , generalDisallowedHeaders ) ; const numberOfHeaders = headers . length ; if ( numberOfHeaders > 0 ) { const message = getMessage ( 'disallowedHeaders' , context . language , headers . join ( ', ' ) ) ; const codeSnippet = headers . reduce ( ( total , header ) => { return `${ total }${ total ? '\n' : '' }${ header }: ${ normalizeHeaderValue ( response . headers , header ) }` ; } , '' ) ; const codeLanguage = 'http' ; context . report ( resource , message , { codeLanguage , codeSnippet , severity : Severity . warning } ) ; } } ; const validateServerHeader = ( { response , resource } ) => { const serverHeaderValue = normalizeHeaderValue ( response . headers , 'server' ) ; const codeLanguage = 'http' ; if ( ! generalDisallowedHeaders . includes ( 'server' ) && ! toLowerCaseArray ( ignoreHeaders ) . includes ( 'server' ) && serverHeaderValue && serverHeaderContainsTooMuchInformation ( serverHeaderValue ) ) { const message = getMessage ( 'headerValueShouldOnlyContain' , context . language ) ; context . report ( resource , message , { codeLanguage , codeSnippet : `Server: ${ serverHeaderValue }` , severity : Severity . warning } ) ; } } ; const validateSpecialHeaders = ( { response , resource } ) => { const codeLanguage = 'http' ; for ( const key of Object . keys ( response . headers ) ) { const lowercaseKey = key . toLowerCase ( ) ; const messageName = specialDisallowedHeaders [ lowercaseKey ] ; if ( messageName && ! toLowerCaseArray ( ignoreHeaders ) . includes ( lowercaseKey ) ) { const message = getMessage ( messageName , context . language ) ; const headerValue = normalizeHeaderValue ( response . headers , lowercaseKey ) ; context . report ( resource , message , { codeLanguage , codeSnippet : `${ key }: ${ headerValue }` , severity : Severity . warning } ) ; } } } ; const validate = ( event ) => { if ( isDataURI ( event . resource ) ) { debug ( `Check does not apply for data URI: ${ event . resource }` ) ; return ; } validateGeneralHeaders ( event ) ; validateServerHeader ( event ) ; validateSpecialHeaders ( event ) ; } ; loadHintConfigs ( ) ; context . on ( 'fetch::end::*' , validate ) ; } }