Skip to content

Commit 95a82f7

Browse files
feat(apps): ad-hoc redaction for apps logs (RocketChat#40096)
Co-authored-by: Diego Sampaio <chinello@gmail.com>
1 parent f4dfb8d commit 95a82f7

17 files changed

Lines changed: 213 additions & 24 deletions

File tree

.changeset/grumpy-ligers-drum.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@rocket.chat/server-fetch': minor
3+
---
4+
5+
Introduces redaction of potentially sensitive data when logging request URLs

.changeset/smart-chicken-repair.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@rocket.chat/meteor': minor
3+
---
4+
5+
Introduces redaction of potentially sensitive data in logs related to apps-engine

.changeset/tender-spies-give.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@rocket.chat/tools': minor
3+
---
4+
5+
Adds new function for censoring URL components in logs

apps/meteor/app/api/server/middlewares/logger.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Logger } from '@rocket.chat/logger';
2+
import { censorUrl } from '@rocket.chat/tools';
23
import type { MiddlewareHandler } from 'hono';
34

45
import { getRestPayload } from '../../../../server/lib/logger/logPayloads';
@@ -11,7 +12,7 @@ export const loggerMiddleware =
1112
const log = logger.logger.child(
1213
{
1314
method: c.req.method,
14-
url: c.req.url,
15+
url: censorUrl(c.req.url),
1516
userId: c.req.header('x-user-id'),
1617
userAgent: c.req.header('user-agent'),
1718
length: c.req.header('content-length'),

apps/meteor/app/apps/server/bridges/http.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { IHttpResponse } from '@rocket.chat/apps-engine/definition/accessor
33
import type { IHttpBridgeRequestInfo } from '@rocket.chat/apps-engine/server/bridges';
44
import { HttpBridge } from '@rocket.chat/apps-engine/server/bridges/HttpBridge';
55
import { serverFetch as fetch, type ExtendedFetchOptions } from '@rocket.chat/server-fetch';
6+
import { censorUrl } from '@rocket.chat/tools';
67

78
import { settings } from '../../../settings/server';
89

@@ -72,7 +73,7 @@ export class AppHttpBridge extends HttpBridge {
7273

7374
// end comptability with old HTTP.call API
7475

75-
this.orch.debugLog(`The App ${info.appId} is requesting from the outter webs:`, info);
76+
this.orch.debugLog({ msg: `The App ${info.appId} is requesting from the outter webs:`, info: { ...info, url: censorUrl(info.url) } });
7677

7778
const shouldIgnoreSsrf = request.ssrfValidation !== true;
7879
const fetchOptions: ExtendedFetchOptions = {

apps/meteor/app/apps/server/bridges/persistence.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export class AppPersistenceBridge extends PersistenceBridge {
1515
}
1616

1717
protected async create(data: object, appId: string): Promise<string> {
18-
this.orch.debugLog(`The App ${appId} is storing a new object in their persistence.`, data);
18+
this.orch.debugLog(`The App ${appId} is storing a new object in their persistence.`);
1919

2020
if (typeof data !== 'object') {
2121
throw new Error('Attempted to store an invalid data type, it must be an object.');
@@ -28,11 +28,10 @@ export class AppPersistenceBridge extends PersistenceBridge {
2828
}
2929

3030
protected async createWithAssociations(data: object, associations: Array<RocketChatAssociationRecord>, appId: string): Promise<string> {
31-
this.orch.debugLog(
32-
`The App ${appId} is storing a new object in their persistence that is associated with some models.`,
33-
data,
31+
this.orch.debugLog({
32+
msg: `The App ${appId} is storing a new object in their persistence that is associated with some models.`,
3433
associations,
35-
);
34+
});
3635

3736
if (typeof data !== 'object') {
3837
throw new Error('Attempted to store an invalid data type, it must be an object.');
@@ -53,7 +52,7 @@ export class AppPersistenceBridge extends PersistenceBridge {
5352
}
5453

5554
protected async readByAssociations(associations: Array<RocketChatAssociationRecord>, appId: string): Promise<Array<object>> {
56-
this.orch.debugLog(`The App ${appId} is searching for records that are associated with the following:`, associations);
55+
this.orch.debugLog({ msg: `The App ${appId} is searching for records that are associated with the following:`, associations });
5756

5857
const records = await this.orch
5958
.getPersistenceModel()
@@ -84,7 +83,7 @@ export class AppPersistenceBridge extends PersistenceBridge {
8483
associations: Array<RocketChatAssociationRecord>,
8584
appId: string,
8685
): Promise<Array<object> | undefined> {
87-
this.orch.debugLog(`The App ${appId} is removing records with the following associations:`, associations);
86+
this.orch.debugLog({ msg: `The App ${appId} is removing records with the following associations:`, associations });
8887

8988
const query = {
9089
appId,
@@ -105,7 +104,7 @@ export class AppPersistenceBridge extends PersistenceBridge {
105104
}
106105

107106
protected async update(id: string, data: object, _upsert: boolean, appId: string): Promise<string> {
108-
this.orch.debugLog(`The App ${appId} is updating the record "${id}" to:`, data);
107+
this.orch.debugLog(`The App ${appId} is updating the record "${id}"`);
109108

110109
if (typeof data !== 'object') {
111110
throw new Error('Attempted to store an invalid data type, it must be an object.');
@@ -120,7 +119,7 @@ export class AppPersistenceBridge extends PersistenceBridge {
120119
upsert = true,
121120
appId: string,
122121
): Promise<string> {
123-
this.orch.debugLog(`The App ${appId} is updating the record with association to data as follows:`, associations, data);
122+
this.orch.debugLog({ msg: `The App ${appId} is updating the record with association to data as follows:`, associations });
124123

125124
if (typeof data !== 'object') {
126125
throw new Error('Attempted to store an invalid data type, it must be an object.');
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import fastRedact from 'fast-redact';
2+
3+
const requestFields = [
4+
'headers.Cookie',
5+
'headers.cookie',
6+
'headers["x-auth-token"]',
7+
'headers["X-Auth-Token"]',
8+
'headers.auth',
9+
'headers.Auth',
10+
'headers.authorization',
11+
'headers.Authorization',
12+
'headers.access_token',
13+
'content.password',
14+
'content.pass',
15+
'data.password',
16+
'data.pass',
17+
];
18+
19+
const entityFields = ['password', 'pass', 'customFields.*', '_unmappedProperties_'];
20+
21+
const roomFields = ['customFields.*', '_unmappedProperties_', ...entityFields.map((field) => `creator.${field}`)];
22+
23+
export const redactionFieldPaths = [
24+
// Incoming requests to the Apps API endpoints
25+
...requestFields,
26+
...entityFields.map((field) => `user.${field}`),
27+
'query.access_token',
28+
'query.query', // The deprecated `query` search param
29+
// Outgoing requests from the Apps to the outter webs
30+
...requestFields.map((field) => `request.${field}`),
31+
`request.query`, // `query` here is a string, so we have to redact it all
32+
// Slashcommands
33+
...roomFields.map((field) => `params[0].room.${field}`),
34+
...entityFields.map((field) => `params[0].sender.${field}`),
35+
];
36+
37+
export const redact = fastRedact({
38+
paths: redactionFieldPaths,
39+
censor: '[Redacted]',
40+
serialize: false,
41+
strict: false,
42+
});

apps/meteor/ee/server/apps/orchestrator.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { AppLogs, Apps as AppsModel, AppsPersistence, Statistics } from '@rocket
1010
import { Meteor } from 'meteor/meteor';
1111

1212
import { AppServerNotifier, AppsRestApi, AppUIKitInteractionApi } from './communication';
13+
import { redactionFieldPaths } from './lib/redactor';
1314
import { MarketplaceAPIClient } from './marketplace/MarketplaceAPIClient';
1415
import { isTesting } from './marketplace/isTesting';
1516
import { AppRealLogStorage, AppRealStorage, ConfigurableAppSourceStorage } from './storage';
@@ -44,7 +45,7 @@ export class AppServerOrchestrator {
4445
return;
4546
}
4647

47-
this._rocketchatLogger = new Logger('Rocket.Chat Apps');
48+
this._rocketchatLogger = new Logger('Rocket.Chat Apps', { redact: redactionFieldPaths });
4849

4950
this._model = AppsModel;
5051
this._logModel = AppLogs;

apps/meteor/ee/server/apps/storage/AppRealLogStorage.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { AppLogStorage } from '@rocket.chat/apps-engine/server/storage';
44
import { InstanceStatus } from '@rocket.chat/instance-status';
55
import type { AppLogs } from '@rocket.chat/models';
66

7+
import { redact } from '../lib/redactor';
8+
79
export class AppRealLogStorage extends AppLogStorage {
810
constructor(private db: typeof AppLogs) {
911
super('mongodb');
@@ -41,6 +43,10 @@ export class AppRealLogStorage extends AppLogStorage {
4143
async storeEntries(logEntry: ILoggerStorageEntry): Promise<ILoggerStorageEntry> {
4244
logEntry.instanceId = InstanceStatus.id();
4345

46+
logEntry.entries.forEach((entry) => {
47+
entry.args.forEach(redact);
48+
});
49+
4450
const id = (await this.db.insertOne(logEntry)).insertedId;
4551

4652
return this.db.findOneById(id);

apps/meteor/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@
204204
"expiry-map": "^2.0.0",
205205
"express": "^4.21.2",
206206
"express-rate-limit": "^5.5.1",
207+
"fast-redact": "^3.5.0",
207208
"fastq": "^1.17.1",
208209
"fflate": "^0.8.2",
209210
"file-type": "^16.5.4",
@@ -353,6 +354,7 @@
353354
"@types/ejson": "^2.2.2",
354355
"@types/express": "^4.17.25",
355356
"@types/express-rate-limit": "^5.1.3",
357+
"@types/fast-redact": "^3",
356358
"@types/google-libphonenumber": "^7.4.30",
357359
"@types/gravatar": "^1.8.6",
358360
"@types/he": "^1.2.3",

0 commit comments

Comments
 (0)