diff --git a/packages/techdocs-common/src/stages/publish/helpers.test.ts b/packages/techdocs-common/src/stages/publish/helpers.test.ts index d5af98e7e1..7eb591bdb2 100644 --- a/packages/techdocs-common/src/stages/publish/helpers.test.ts +++ b/packages/techdocs-common/src/stages/publish/helpers.test.ts @@ -21,11 +21,17 @@ import { getFileTreeRecursively, getHeadersForFileExtension } from './helpers'; describe('getHeadersForFileExtension', () => { const correctMapOfExtensions = [ ['.html', 'text/plain; charset=utf-8'], + ['.htm', 'text/plain; charset=utf-8'], + ['.HTML', 'text/plain; charset=utf-8'], + ['.dhtml', 'text/plain; charset=utf-8'], + ['.xhtml', 'text/plain; charset=utf-8'], + ['.xml', 'text/plain; charset=utf-8'], ['.css', 'text/css; charset=utf-8'], ['.png', 'image/png'], ['.jpg', 'image/jpeg'], ['.jpeg', 'image/jpeg'], ['.svg', 'text/plain; charset=utf-8'], + ['.SVG', 'text/plain; charset=utf-8'], ['.json', 'application/json; charset=utf-8'], ['.this-in-not-an-extension', 'text/plain; charset=utf-8'], ]; diff --git a/packages/techdocs-common/src/stages/publish/helpers.ts b/packages/techdocs-common/src/stages/publish/helpers.ts index 3ca424055f..22da949aec 100644 --- a/packages/techdocs-common/src/stages/publish/helpers.ts +++ b/packages/techdocs-common/src/stages/publish/helpers.ts @@ -16,6 +16,22 @@ import mime from 'mime-types'; import recursiveReadDir from 'recursive-readdir'; +/** + * Helper to get the expected content-type for a given file extension. Also + * takes XSS mitigation into account. + */ +const getContentTypeForExtension = (ext: string): string => { + const defaultContentType = 'text/plain; charset=utf-8'; + + // Prevent sanitization bypass by preventing browsers from directly rendering + // the contents of untrusted files. + if (ext.match(/htm|xml|svg/i)) { + return defaultContentType; + } + + return mime.contentType(ext) || defaultContentType; +}; + export type responseHeadersType = { 'Content-Type': string; }; @@ -28,18 +44,9 @@ export type responseHeadersType = { export const getHeadersForFileExtension = ( fileExtension: string, ): responseHeadersType => { - const headerType = { - 'Content-Type': - mime.contentType(fileExtension) || 'text/plain; charset=utf-8', + return { + 'Content-Type': getContentTypeForExtension(fileExtension), }; - - // Prevent sanitization bypass by preventing browers from directly rendering - // the contents of untrusted content. - if (headerType['Content-Type'].match(/html|xml/)) { - headerType['Content-Type'] = 'text/plain; charset=utf-8'; - } - - return headerType; }; /** diff --git a/packages/techdocs-common/src/stages/publish/local.ts b/packages/techdocs-common/src/stages/publish/local.ts index 036053627b..bf49c5b926 100644 --- a/packages/techdocs-common/src/stages/publish/local.ts +++ b/packages/techdocs-common/src/stages/publish/local.ts @@ -137,10 +137,10 @@ export class LocalPublish implements PublisherBase { // Handle content-type header the same as all other publishers. setHeaders: (res, filePath) => { const fileExtension = path.extname(filePath); - const { 'Content-Type': header } = getHeadersForFileExtension( - fileExtension, - ); - res.setHeader('Content-Type', header); + const headers = getHeadersForFileExtension(fileExtension); + for (const [header, value] of Object.entries(headers)) { + res.setHeader(header, value); + } }, }); }