[8.11] [logging] Encode control chars (#169773) (#170032)

# Backport

This will backport the following commits from `main` to `8.11`:
- [[logging] Encode control chars
(#169773)](https://github.com/elastic/kibana/pull/169773)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Alejandro Fernández
Haro","email":"alejandro.haro@elastic.co"},"sourceCommit":{"committedDate":"2023-10-27T13:35:37Z","message":"[logging]
Encode control chars
(#169773)","sha":"9c58bd5ec892db62794afbe3572ed2ba5d9d57fc","branchLabelMapping":{"^v8.12.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","Team:Core","Team:Security","release_note:skip","Feature:Logging","backport:prev-minor","v8.12.0"],"number":169773,"url":"https://github.com/elastic/kibana/pull/169773","mergeCommit":{"message":"[logging]
Encode control chars
(#169773)","sha":"9c58bd5ec892db62794afbe3572ed2ba5d9d57fc"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.12.0","labelRegex":"^v8.12.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/169773","number":169773,"mergeCommit":{"message":"[logging]
Encode control chars
(#169773)","sha":"9c58bd5ec892db62794afbe3572ed2ba5d9d57fc"}}]}]
BACKPORT-->

Co-authored-by: Alejandro Fernández Haro <alejandro.haro@elastic.co>
This commit is contained in:
Kibana Machine 2023-10-27 10:51:16 -04:00 committed by GitHub
parent 1c56c2ccd8
commit 325896e06d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 35 additions and 10 deletions

View file

@ -24,16 +24,16 @@ describe('MessageConversion', () => {
).toEqual('Hi!\nHow are you?');
});
test('it should remove ANSI chars lines from the message', () => {
test('it should encode/escape ANSI chars lines from the message', () => {
expect(
MessageConversion.convert(
{ ...baseRecord, message: 'Blinking...\u001b[5;7;6mThis is Fine\u001b[27m' },
false
)
).toEqual('Blinking...This is Fine');
).toEqual('Blinking...\\u001b[5;7;6mThis is Fine\\u001b[27m');
});
test('it should remove any unicode injection from the message', () => {
test('it should encode/escape any unicode injection from the message', () => {
expect(
MessageConversion.convert(
{
@ -43,6 +43,23 @@ describe('MessageConversion', () => {
},
false
)
).toEqual('ESC-INJECTION-LFUNICODE:SUCCESSFUL\n\nInjecting 10.000 lols 😂');
).toEqual(
'\\u001b[31mESC-INJECTION-LFUNICODE:\\u001b[32mSUCCESSFUL\\u001b[0m\\u0007\n\nInjecting 10.000 lols 😂\\u001b[10000;b\\u0007'
);
});
test('it should encode/escape any unicode injection from the message (including nullbyte)', () => {
expect(
MessageConversion.convert(
{
...baseRecord,
message:
'\u001b\u0000[31mESC-INJECTION-LFUNICODE:\u001b[32mSUCCESSFUL\u001b[0m\u0007\n\nInjecting 10.000 lols 😂\u001b[10000;b\u0007',
},
false
)
).toEqual(
'\\u001b\\u0000[31mESC-INJECTION-LFUNICODE:\\u001b[32mSUCCESSFUL\\u001b[0m\\u0007\n\nInjecting 10.000 lols 😂\\u001b[10000;b\\u0007'
);
});
});

View file

@ -6,20 +6,28 @@
* Side Public License, v 1.
*/
import ansiRegex from 'ansi-regex';
import { LogRecord } from '@kbn/logging';
import { Conversion } from './types';
// Defining it globally because it's more performant than creating for each log entry
// We can reuse the same global RegExp here because `.replace()` automatically resets the `.lastIndex` of the RegExp.
const ANSI_ESCAPE_CODES_REGEXP = ansiRegex();
// From https://www.ascii-code.com/characters/control-characters,
// but explicitly allowing the range \u0008-\u000F (line breaks, tabs, etc.)
const CONTROL_CHAR_REGEXP = new RegExp('[\\u0000-\\u0007\\u0010-\\u001F]', 'g');
export const MessageConversion: Conversion = {
pattern: /%message/g,
convert(record: LogRecord) {
// Error stack is much more useful than just the message.
const str = record.error?.stack || record.message;
// We need to validate it's a string because, despite types, there are use case where it's not a string :/
return typeof str === 'string' ? str.replace(ANSI_ESCAPE_CODES_REGEXP, '') : str;
return typeof str === 'string' // We need to validate it's a string because, despite types, there are use case where it's not a string :/
? str.replace(
CONTROL_CHAR_REGEXP,
// Escaping control chars via JSON.stringify to maintain consistency with `meta` and the JSON layout.
// This way, post analysis of the logs is easier as we can search the same patterns.
// Our benchmark didn't show a big difference in performance between custom-escaping vs. JSON.stringify one.
// The slice is removing the double-quotes.
(substr) => JSON.stringify(substr).slice(1, -1)
)
: str;
},
};