From 3b0bee717a68d7be6671ffc3724a7c22217541f1 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Thu, 16 Feb 2023 16:45:34 +0800 Subject: [PATCH] fix(core): mask password in audit log (#3130) --- .changeset-staged/afraid-eagles-retire.md | 5 +++ .../core/src/middleware/koa-audit-log.test.ts | 38 +++++++++++++++++++ packages/core/src/middleware/koa-audit-log.ts | 17 ++++++++- 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 .changeset-staged/afraid-eagles-retire.md diff --git a/.changeset-staged/afraid-eagles-retire.md b/.changeset-staged/afraid-eagles-retire.md new file mode 100644 index 000000000..2cc2ce1f0 --- /dev/null +++ b/.changeset-staged/afraid-eagles-retire.md @@ -0,0 +1,5 @@ +--- +"@logto/core": minor +--- + +- mask sensitive password value in audit logs diff --git a/packages/core/src/middleware/koa-audit-log.test.ts b/packages/core/src/middleware/koa-audit-log.test.ts index a6c90be86..1776889f8 100644 --- a/packages/core/src/middleware/koa-audit-log.test.ts +++ b/packages/core/src/middleware/koa-audit-log.test.ts @@ -122,6 +122,44 @@ describe('koaAuditLog middleware', () => { expect(insertLog).not.toBeCalled(); }); + it('should filter password sensitive data in log', async () => { + // @ts-expect-error for testing + const ctx: WithLogContext> = { + ...createContextWithRouteParameters({ headers: { 'user-agent': userAgent } }), + }; + ctx.request.ip = ip; + + const additionalMockPayload = { + password: '123456', + interaction: { profile: { password: 123_456 } }, + }; + + const maskedAdditionalMockPayload = { + password: '******', + interaction: { profile: { password: '******' } }, + }; + + const next = async () => { + const log = ctx.createLog(logKey); + log.append(mockPayload); + log.append(additionalMockPayload); + }; + await koaLog(queries)(ctx, next); + + expect(insertLog).toBeCalledWith({ + id: nanoIdMock, + key: logKey, + payload: { + ...mockPayload, + ...maskedAdditionalMockPayload, + key: logKey, + result: LogResult.Success, + ip, + userAgent, + }, + }); + }); + describe('should insert an error log with the error message when next() throws an error', () => { it('should log with error message when next throws a normal Error', async () => { // @ts-expect-error for testing diff --git a/packages/core/src/middleware/koa-audit-log.ts b/packages/core/src/middleware/koa-audit-log.ts index 877e5bb61..5af877d5b 100644 --- a/packages/core/src/middleware/koa-audit-log.ts +++ b/packages/core/src/middleware/koa-audit-log.ts @@ -8,6 +8,21 @@ import type { IRouterParamContext } from 'koa-router'; import RequestError from '#src/errors/RequestError/index.js'; import type Queries from '#src/tenants/Queries.js'; +const isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null && !Array.isArray(value); + +const filterSensitiveData = (data: Record): Record => { + return Object.fromEntries( + Object.entries(data).map(([key, value]) => { + if (isRecord(value)) { + return [key, filterSensitiveData(value)]; + } + + return [key, key === 'password' ? '******' : value]; + }) + ); +}; + const removeUndefinedKeys = (object: Record) => Object.fromEntries(Object.entries(object).filter(([, value]) => value !== undefined)); @@ -33,7 +48,7 @@ export class LogEntry { append(data: Readonly) { this.payload = { ...this.payload, - ...removeUndefinedKeys(data), + ...filterSensitiveData(removeUndefinedKeys(data)), }; } }