diff --git a/package-lock.json b/package-lock.json index ddec81b71d..4e91464119 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7404,7 +7404,8 @@ "bowser": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.9.0.tgz", - "integrity": "sha512-2ld76tuLBNFekRgmJfT2+3j5MIrP6bFict8WAIT3beq+srz1gcKNAdNKMqHqauQt63NmAa88HfP1/Ypa9Er3HA==" + "integrity": "sha512-2ld76tuLBNFekRgmJfT2+3j5MIrP6bFict8WAIT3beq+srz1gcKNAdNKMqHqauQt63NmAa88HfP1/Ypa9Er3HA==", + "dev": true }, "boxicons": { "version": "1.8.0", @@ -7879,11 +7880,6 @@ "quick-lru": "^4.0.1" } }, - "camelize": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", - "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" - }, "caniuse-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", @@ -8678,11 +8674,6 @@ } } }, - "content-security-policy-builder": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/content-security-policy-builder/-/content-security-policy-builder-2.1.0.tgz", - "integrity": "sha512-/MtLWhJVvJNkA9dVLAp6fg9LxD2gfI6R2Fi1hPmfjYXSahJJzcfvoeDOxSyp4NvxMuwWv3WMssE9o31DoULHrQ==" - }, "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", @@ -9439,11 +9430,6 @@ "assert-plus": "^1.0.0" } }, - "dasherize": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dasherize/-/dasherize-2.0.0.tgz", - "integrity": "sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg=" - }, "data-uri-to-buffer": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz", @@ -10253,11 +10239,6 @@ } } }, - "dns-prefetch-control": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dns-prefetch-control/-/dns-prefetch-control-0.2.0.tgz", - "integrity": "sha512-hvSnros73+qyZXhHFjx2CMLwoj3Fe7eR9EJsFsqmcI1bB2OBWL/+0YzaEaKssCHnj/6crawNnUyw74Gm2EKe+Q==" - }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -10326,11 +10307,6 @@ "domhandler": "^3.0.0" } }, - "dont-sniff-mimetype": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/dont-sniff-mimetype/-/dont-sniff-mimetype-1.1.0.tgz", - "integrity": "sha512-ZjI4zqTaxveH2/tTlzS1wFp+7ncxNZaIEWYg3lzZRHkKf5zPT/MnEG6WL0BhHMJUabkh8GeU5NL5j+rEUCb7Ug==" - }, "dot-prop": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", @@ -11573,11 +11549,6 @@ } } }, - "expect-ct": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/expect-ct/-/expect-ct-0.2.0.tgz", - "integrity": "sha512-6SK3MG/Bbhm8MsgyJAylg+ucIOU71/FzyFalcfu5nY19dH8y/z0tBJU0wrNBXD4B27EoQtqPF/9wqH0iYAd04g==" - }, "expiry-map": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/expiry-map/-/expiry-map-1.1.0.tgz", @@ -11901,11 +11872,6 @@ "pend": "~1.2.0" } }, - "feature-policy": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/feature-policy/-/feature-policy-0.3.0.tgz", - "integrity": "sha512-ZtijOTFN7TzCujt1fnNhfWPFPSHeZkesff9AXZj+UEjYBynWNUIYpC87Ve4wHzyexQsImicLu7WsC2LHq7/xrQ==" - }, "fecha": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", @@ -12270,11 +12236,6 @@ "map-cache": "^0.2.2" } }, - "frameguard": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/frameguard/-/frameguard-3.1.0.tgz", - "integrity": "sha512-TxgSKM+7LTA6sidjOiSZK9wxY0ffMPY3Wta//MqwmX0nZuEHc8QrkV8Fh3ZhMJeiH+Uyh/tcaarImRy8u77O7g==" - }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -13024,48 +12985,9 @@ "dev": true }, "helmet": { - "version": "3.23.1", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-3.23.1.tgz", - "integrity": "sha512-e034HHfRK4065BFjYbffn5jXaTWWrhTNgmLIppsGEOjpdDB1MBQkWlAFW/auULXAu6uKk2X76n7a7gvz5sSjkg==", - "requires": { - "depd": "2.0.0", - "dns-prefetch-control": "0.2.0", - "dont-sniff-mimetype": "1.1.0", - "expect-ct": "0.2.0", - "feature-policy": "0.3.0", - "frameguard": "3.1.0", - "helmet-crossdomain": "0.4.0", - "helmet-csp": "2.10.0", - "hide-powered-by": "1.1.0", - "hpkp": "2.0.0", - "hsts": "2.2.0", - "nocache": "2.1.0", - "referrer-policy": "1.2.0", - "x-xss-protection": "1.3.0" - }, - "dependencies": { - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - } - } - }, - "helmet-crossdomain": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/helmet-crossdomain/-/helmet-crossdomain-0.4.0.tgz", - "integrity": "sha512-AB4DTykRw3HCOxovD1nPR16hllrVImeFp5VBV9/twj66lJ2nU75DP8FPL0/Jp4jj79JhTfG+pFI2MD02kWJ+fA==" - }, - "helmet-csp": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/helmet-csp/-/helmet-csp-2.10.0.tgz", - "integrity": "sha512-Rz953ZNEFk8sT2XvewXkYN0Ho4GEZdjAZy4stjiEQV3eN7GDxg1QKmYggH7otDyIA7uGA6XnUMVSgeJwbR5X+w==", - "requires": { - "bowser": "2.9.0", - "camelize": "1.0.0", - "content-security-policy-builder": "2.1.0", - "dasherize": "2.0.0" - } + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-4.1.0.tgz", + "integrity": "sha512-KWy75fYN8hOG2Rhl8e5B3WhOzb0by1boQum85TiddIE9iu6gV+TXbUjVC17wfej0o/ZUpqB9kxM0NFCZRMzf+Q==" }, "hex-color-regex": { "version": "1.1.0", @@ -13073,11 +12995,6 @@ "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", "dev": true }, - "hide-powered-by": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/hide-powered-by/-/hide-powered-by-1.1.0.tgz", - "integrity": "sha512-Io1zA2yOA1YJslkr+AJlWSf2yWFkKjvkcL9Ni1XSUqnGLr/qRQe2UI3Cn/J9MsJht7yEVCe0SscY1HgVMujbgg==" - }, "highlight-es": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/highlight-es/-/highlight-es-1.0.3.tgz", @@ -13133,11 +13050,6 @@ "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", "dev": true }, - "hpkp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hpkp/-/hpkp-2.0.0.tgz", - "integrity": "sha1-EOFCJk52IVpdMMROxD3mTe5tFnI=" - }, "hsl-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", @@ -13150,21 +13062,6 @@ "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", "dev": true }, - "hsts": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/hsts/-/hsts-2.2.0.tgz", - "integrity": "sha512-ToaTnQ2TbJkochoVcdXYm4HOCliNozlviNsg+X2XQLQvZNI/kCHR9rZxVYpJB3UPcHz80PgxRyWQ7PdU1r+VBQ==", - "requires": { - "depd": "2.0.0" - }, - "dependencies": { - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - } - } - }, "html-comment-regex": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", @@ -21615,11 +21512,6 @@ "esprima": "~3.0.0" } }, - "referrer-policy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.2.0.tgz", - "integrity": "sha512-LgQJIuS6nAy1Jd88DCQRemyE3mS+ispwlqMk3b0yjZ257fI1v9c+/p6SD5gP5FGyXUIgrNOAfmyioHwZtYv2VA==" - }, "regenerate": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.1.tgz", @@ -27055,11 +26947,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz", "integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==" }, - "x-xss-protection": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/x-xss-protection/-/x-xss-protection-1.3.0.tgz", - "integrity": "sha512-kpyBI9TlVipZO4diReZMAHWtS0MMa/7Kgx8hwG/EuZLiA6sg4Ah/4TRdASHhRRN3boobzcYgFRUFSgHRge6Qhg==" - }, "xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", diff --git a/package.json b/package.json index 9f6519252b..e267768ecd 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,7 @@ "font-awesome": "4.7.0", "glob": "^7.1.2", "has-ansi": "^4.0.0", - "helmet": "^3.21.3", + "helmet": "^4.1.0", "http-status-codes": "^2.1.2", "intl-tel-input": "~12.1.6", "json-stringify-deterministic": "^1.0.1", @@ -139,6 +139,7 @@ "ng-infinite-scroll": "^1.3.0", "ng-table": "^3.0.1", "ngclipboard": "^2.0.0", + "nocache": "^2.1.0", "node-cache": "^5.1.2", "node-jose": "^1.0.0", "nodemailer": "^6.4.11", @@ -250,4 +251,4 @@ "webpack-merge": "^4.1.3", "worker-loader": "^2.0.0" } -} +} \ No newline at end of file diff --git a/src/loaders/express/__tests__/helmet.spec.ts b/src/loaders/express/__tests__/helmet.spec.ts new file mode 100644 index 0000000000..0f8b1cadc4 --- /dev/null +++ b/src/loaders/express/__tests__/helmet.spec.ts @@ -0,0 +1,170 @@ +import helmet from 'helmet' +import expressHandler from 'tests/unit/backend/helpers/jest-express' +import { mocked } from 'ts-jest/utils' + +import config from 'src/config/config' +import featureManager from 'src/config/feature-manager' + +import helmetMiddlewares from '../helmet' + +describe('helmetMiddlewares', () => { + jest.mock('helmet') + const mockHelmet = mocked(helmet, true) + jest.mock('src/config/feature-manager') + const mockFeatureManager = mocked(featureManager, true) + jest.mock('src/config/config') + const mockConfig = mocked(config, true) + + const cspCoreDirectives = { + defaultSrc: ["'self'"], + imgSrc: [ + "'self'", + 'data:', + 'https://www.googletagmanager.com/', + 'https://www.google-analytics.com/', + `https://s3-${config.aws.region}.amazonaws.com/agency.form.sg/`, + config.aws.imageBucketUrl, + config.aws.logoBucketUrl, + '*', + ], + fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com/'], + scriptSrc: [ + "'self'", + 'https://www.googletagmanager.com/', + 'https://ssl.google-analytics.com/', + 'https://www.google-analytics.com/', + 'https://www.tagmanager.google.com/', + 'https://www.google.com/recaptcha/', + 'https://www.recaptcha.net/recaptcha/', + 'https://www.gstatic.com/recaptcha/', + 'https://www.gstatic.cn/', + 'https://www.google-analytics.com/', + ], + connectSrc: [ + "'self'", + 'https://www.google-analytics.com/', + 'https://ssl.google-analytics.com/', + 'https://sentry.io/api/', + config.aws.attachmentBucketUrl, + config.aws.imageBucketUrl, + config.aws.logoBucketUrl, + ], + frameSrc: [ + "'self'", + 'https://www.google.com/recaptcha/', + 'https://www.recaptcha.net/recaptcha/', + ], + objectSrc: ["'none'"], + styleSrc: [ + "'self'", + 'https://www.google.com/recaptcha/', + 'https://www.recaptcha.net/recaptcha/', + 'https://www.gstatic.com/recaptcha/', + 'https://www.gstatic.cn/', + // For inline styles in angular-sanitize.js + "'sha256-b3IrgBVvuKx/Q3tmAi79fnf6AFClibrz/0S5x1ghdGU='", + ], + formAction: ["'self'"], + } + + beforeAll(() => { + mockHelmet.xssFilter = jest.fn().mockReturnValue('xssFilter') + mockHelmet.noSniff = jest.fn().mockReturnValue('noSniff') + mockHelmet.ieNoOpen = jest.fn().mockReturnValue('ieNoOpen') + mockHelmet.dnsPrefetchControl = jest + .fn() + .mockReturnValue('dnsPrefetchControl') + mockHelmet.hidePoweredBy = jest.fn().mockReturnValue('hidePoweredBy') + mockHelmet.referrerPolicy = jest.fn().mockReturnValue('referrerPolicy') + mockHelmet.contentSecurityPolicy = jest + .fn() + .mockReturnValue('contentSecurityPolicy') + mockHelmet.hsts = jest.fn().mockReturnValue(jest.fn()) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it('should call the correct helmet functions', () => { + helmetMiddlewares() + expect(mockHelmet.xssFilter).toHaveBeenCalled() + expect(mockHelmet.noSniff).toHaveBeenCalled() + expect(mockHelmet.ieNoOpen).toHaveBeenCalled() + expect(mockHelmet.dnsPrefetchControl).toHaveBeenCalled() + expect(mockHelmet.hidePoweredBy).toHaveBeenCalled() + expect(mockHelmet.referrerPolicy).toHaveBeenCalled() + expect(mockHelmet.contentSecurityPolicy).toHaveBeenCalled() + }) + + it('should call helmet.hsts() if req.secure', () => { + const mockReq = expressHandler.mockRequest({ secure: true }) + const mockRes = expressHandler.mockResponse() + const mockNext = jest.fn() + + // Find works for helmet.hsts() because the other functions are mocked to return a string + const hstsFn = helmetMiddlewares().find( + (result) => typeof result === 'function', + ) + // Necessary to check for hstsFn because find() returns undefined by default, otherwise + // will throw TypeError + if (hstsFn) { + hstsFn(mockReq, mockRes, mockNext) + } + expect(mockHelmet.hsts).toHaveBeenCalledWith({ maxAge: 5184000 }) + expect(mockNext).not.toHaveBeenCalled() + }) + + it('should not call helmet.hsts() if !req.secure', () => { + const mockReq = expressHandler.mockRequest({ secure: false }) + const mockRes = expressHandler.mockResponse() + const mockNext = jest.fn() + + const hstsFn = helmetMiddlewares().find( + (result) => typeof result === 'function', + ) + if (hstsFn) { + hstsFn(mockReq, mockRes, mockNext) + } + + expect(mockHelmet.hsts).not.toHaveBeenCalled() + expect(mockNext).toHaveBeenCalled() + }) + + it('should call helmet.contentSecurityPolicy() with the correct directives if cspReportUri and !isDev', () => { + mockFeatureManager.props = jest + .fn() + .mockReturnValue({ cspReportUri: 'value' }) + mockConfig.isDev = false + helmetMiddlewares() + expect(mockHelmet.contentSecurityPolicy).toHaveBeenCalledWith({ + directives: { + ...cspCoreDirectives, + reportUri: ['value'], + upgradeInsecureRequests: [], + }, + }) + }) + + it('should call helmet.contentSecurityPolicy() with the correct directives if !cspReportUri and isDev', () => { + mockFeatureManager.props = jest.fn() + mockConfig.isDev = true + helmetMiddlewares() + expect(mockHelmet.contentSecurityPolicy).toHaveBeenCalledWith({ + directives: { + ...cspCoreDirectives, + }, + }) + }) + + it('should return the correct values from helmet', () => { + const result = helmetMiddlewares() + expect(result).toContain('xssFilter') + expect(result).toContain('noSniff') + expect(result).toContain('ieNoOpen') + expect(result).toContain('dnsPrefetchControl') + expect(result).toContain('hidePoweredBy') + expect(result).toContain('referrerPolicy') + expect(result).toContain('contentSecurityPolicy') + }) +}) diff --git a/src/loaders/express/helmet.ts b/src/loaders/express/helmet.ts index 46a0122584..008c0b909a 100644 --- a/src/loaders/express/helmet.ts +++ b/src/loaders/express/helmet.ts @@ -1,5 +1,6 @@ import { RequestHandler } from 'express' import helmet from 'helmet' +import { ContentSecurityPolicyOptions } from 'helmet/dist/middlewares/content-security-policy' import { get } from 'lodash' import config from '../../config/config' @@ -27,7 +28,7 @@ const helmetMiddlewares = () => { policy: 'strict-origin-when-cross-origin', }) - const cspCoreDirectives = { + const cspCoreDirectives: ContentSecurityPolicyOptions['directives'] = { defaultSrc: ["'self'"], imgSrc: [ "'self'", @@ -77,7 +78,6 @@ const helmetMiddlewares = () => { "'sha256-b3IrgBVvuKx/Q3tmAi79fnf6AFClibrz/0S5x1ghdGU='", ], formAction: ["'self'"], - upgradeInsecureRequests: !config.isDev, } const reportUri = get( @@ -85,7 +85,18 @@ const helmetMiddlewares = () => { 'cspReportUri', undefined, ) - const cspOptionalDirectives = reportUri ? { reportUri } : {} + + const cspOptionalDirectives: ContentSecurityPolicyOptions['directives'] = {} + + // Add on reportUri CSP header if ReportUri exists + // It is necessary to have the if statement for optional directives because falsey values + // do not work - e.g. cspOptionalDirectives.reportUri = [false] will still set the reportUri header + // See https://github.com/helmetjs/csp/issues/36 and + // https://github.com/helmetjs/helmet/blob/cb170160e7c1ccac314cc19d3b979cfc771f1349/middlewares/content-security-policy/index.ts#L135 + if (reportUri) cspOptionalDirectives.reportUri = [reportUri] + + // Add on upgradeInsecureRequest CSP header if !config.isDev + if (!config.isDev) cspOptionalDirectives.upgradeInsecureRequests = [] const contentSecurityPolicyMiddleware = helmet.contentSecurityPolicy({ directives: { diff --git a/src/loaders/express/index.ts b/src/loaders/express/index.ts index 8a2ffe5238..92cf6be6f6 100644 --- a/src/loaders/express/index.ts +++ b/src/loaders/express/index.ts @@ -1,9 +1,9 @@ import compression from 'compression' import express, { Express } from 'express' import device from 'express-device' -import helmet from 'helmet' import http from 'http' import { Connection } from 'mongoose' +import nocache from 'nocache' import path from 'path' import url from 'url' @@ -93,7 +93,7 @@ const loadExpressApp = async (connection: Connection) => { // !!!!! DO NOT CHANGE THE ORDER OF THE NEXT 3 LINES !!!!! // The first line redirects requests to /public/fonts to - // ./dist/frontend/fonts. After that, helmet.noCache() ensures that + // ./dist/frontend/fonts. After that, nocache() ensures that // cache headers are not set on requests for fonts, which ensures that // fonts are shown correctly on IE11. // The last line redirects requests to /public to ./dist/frontend, @@ -103,7 +103,7 @@ const loadExpressApp = async (connection: Connection) => { express.static(path.resolve('./dist/frontend/fonts')), ) - app.use(helmet.noCache()) // Add headers to prevent browser caching front-end code + app.use(nocache()) // Add headers to prevent browser caching front-end code // Setting the app static folder app.use('/public', express.static(path.resolve('./dist/frontend'))) diff --git a/tests/integration/helpers/express-setup.ts b/tests/integration/helpers/express-setup.ts index 994c51853f..03bd044831 100644 --- a/tests/integration/helpers/express-setup.ts +++ b/tests/integration/helpers/express-setup.ts @@ -1,7 +1,7 @@ import compression from 'compression' import express, { Router } from 'express' -import helmet from 'helmet' import mongoose from 'mongoose' +import nocache from 'nocache' import { Response } from 'supertest' import errorHandlerMiddlewares from 'src/loaders/express/error-handler' @@ -20,7 +20,7 @@ export const setupApp = ( app.use(compression()) app.use(parserMiddlewares()) app.use(helmetMiddlewares()) - app.use(helmet.noCache()) + app.use(nocache()) app.use(sessionMiddlewares(mongoose.connection)) @@ -43,7 +43,7 @@ export const setupApp = ( .set('cookie', cookieStore.get()); */ export class CookieStore { - #currentCookie: string = '' + #currentCookie = '' handleCookie(res: Response) { this.set(res.header['set-cookie'][0]) diff --git a/tests/unit/backend/helpers/jest-express.ts b/tests/unit/backend/helpers/jest-express.ts index 46672e5324..ed91e6fed2 100644 --- a/tests/unit/backend/helpers/jest-express.ts +++ b/tests/unit/backend/helpers/jest-express.ts @@ -4,15 +4,18 @@ const mockRequest =
, B>({ params, body, session, + secure, }: { params?: P body?: B session?: any + secure?: boolean } = {}) => { return { body: body ?? {}, params: params ?? {}, session: session ?? {}, + secure: secure ?? true, get(name: string) { if (name === 'cf-connecting-ip') return 'MOCK_IP' return undefined