ReDoS attacks

테스트

example.js
1
2
3
console.time(0);
/^(([a-z])+.)+[A-Z]([a-z])+$/g.test('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!');
console.timeEnd(0);
  • a가 많아질수록 급격하게 평가하는데 오래 걸리는 모습이다
  • 복잡한 정규식을 짤 때 성능 테스트가 동반되어야 할 것 같다
  • 인풋의 길이 등 사전에 필터링해줄 수 있는 것들은 필터링해주면 좋을 것 같다

참고

regexp lastIndex

정규식 test는 true인데 exec가 동작하지 않는다?!

work!
1
2
3
4
5
6
7
8
9
if (regexp.test(line)) {
// 내부 포인터 재설정
regexp.lastIndex = 0;

let match = regexp.exec(line);
while (match) {
// ...
}
}
  • regexp.lastIndex = 0

잘못된 코드

not work!
1
2
3
4
5
6
7
8
if (regexp.test(line)) {
// 여기서 lastIndex 갱신
let match = regexp.exec(line); // 갱신된 lastIndex부터 검색을 시작해서 첫번째 결과가 스킵됨.
while (match) {
// 라인에 일치하는 패턴이 한개라면 반복문한 한번도 돌지 않음
// ...
}
}
  • 처음에 작성한 코드다
  • 파일의 라인 한 줄 한 줄 읽으면서
  • 정규식 패턴과 일치하는 라인이라면
  • 결괏값을 받도록 만들었는데, 이상하게도 제대로 동작하지 않았다
  • 알고 보니까 test() 메서드도 lastIndex를 갱신했었다…
  • 사실 저 if (regexp.test(line)) {은 필요 없는 코드 같다
  • testexec를 같이 쓸 때 주의가 필요하다

참고

정규식으로 숫자를 포함하지 않는 것만 가져오기

1
2
3
4
5
6
7
8
9
const nickNameList = ['chinsung', 'chinsun99999999', '2021', 'regexp'];

const noNumber = nickNameList.filter((nickName) => {
const hasNumber = /\d/;

return !hasNumber.test(nickName);
});

console.log(noNumber);
  • 배열에서 숫자를 포함하는 값만 뽑아보자
  • 또는 숫자를 포함하는 값만 뽑아보자

정규식으로 좌표꺼내기

(37.11111111111111, 126.11111111111111)

  • 이런 좌표를 나타내는 문자열에서 알맹이만 빼보자
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let coordString = '(37.11111111111111, 126.11111111111111)'
/\d+([.]\d+)?/.exec(coordString)
// "37.11111111111111,.11111111111111"

function getPositionFromString(positionString) {
const re = /(\d+)([\.]\d+)?/g;
const result = []

while(match = re.exec(positionString)){
// console.info(match[0], ' found at : ', match.index);
result.push(match[0])
}

// console.info({x:result[0],y:result[1]})
return {x:result[0],y:result[1]}
}
  • exec() 가 g옵션에 따라서 매치하는 패턴의 결과가 모두 담겨서 나오는줄 알았다
  • 위처럼 while 루프를 통해서 모든 매치결과를 받아올 수 있다
  • 내부적으로 커서가 있는 것 같다

참고

정규식 : 특정 문자열을 포함하는 줄, 라인 판단

상황

log
1
2
3
4
5
6
7
"GET / HTTP/1.1" 200 hello world 1
"GET / HTTP/1.1" 200 hello world 2
"GET /a HTTP/1.1" 404 hello world 1
"POST / HTTP/1.1" 404 hello world 1
"GET / HTTP/1.1" 200 hello world 2
"GET / HTTP/1.1" 200 hello world 1
"POST /asd HTTP/1.1" 404 hello world 2
  • 이런 웹 로그가 있다고 하자
  • 중간에 보이는 200, 404 는 status code 이다
  • 404 에러인 라인만 뽑아서 보고싶을 때..

로그파일을 한줄씩 읽으며 정규식으로 확인하기

app.ts 전체코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import * as fs from 'fs';
import * as path from 'path';
import * as readline from 'readline';

const input_log_file_name = 'nohup20201107.out';

const input_file_path = path.join(
__dirname,
`../input files/${input_log_file_name}`
);
const output_file_path = path.join(
__dirname,
`../output files/${input_log_file_name}`
);
console.log(input_file_path);

async function processLineByLine() {
const fileStream = fs.createReadStream(input_file_path);

const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity,
});

// 정규식
const reg404 = /^.*(404).*/;
let result_string = '';

for await (const line of rl) {
// console.log(`Line from file: ${line}`);

if (reg404.test(line)) {
// console.log(`hello : ${line}`);
result_string += line + '\r\n';
}
}

fs.writeFile(output_file_path, result_string, 'utf8', function (err) {
if (err) throw err;
console.log('file write complete');
});
}

processLineByLine();
readline으로 한줄한줄 읽기
1
2
3
4
5
6
const fileStream = fs.createReadStream(input_file_path);

const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity,
});
  • 우선, 파일을 한줄씩 읽기위해 readline을 사용했다
정규식으로 404 문자열 포함 여부 확인
1
2
3
4
5
6
7
8
9
10
11
12
// 정규식
const reg404 = /^.*(404).*/;
let result_string = '';

for await (const line of rl) {
// console.log(`Line from file: ${line}`);

if (reg404.test(line)) {
// console.log(`hello : ${line}`);
result_string += line + '\r\n';
}
}
  • ^.*(404).* 로 정규식을 새웠다
  • ^.* ; 아무 문자로 시작하는 0개 이상의 문자열로 시작하고
  • (404) ; 404 문자열을 중간에 포함하며
  • .* ; 아무 문자열로 끝나는가
  • reg404.test(line)로 한줄한줄 확인하고
  • 참인 경우에 result_string 에 더하기
result_string 결과 새파일로 쓰기
1
2
3
4
fs.writeFile(output_file_path, result_string, 'utf8', function (err) {
if (err) throw err;
console.log('file write complete');
});
  • result_string에는 404 문자열을 포함한느 라인만 추출되었고
  • output_file_path에 해당 내용을 쓴다

[카카오 인턴] 키패드 누르기

[카카오 인턴] 키패드 누르기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// https://programmers.co.kr/learn/courses/30/lessons/67256
function solution(numbers, hand) {
// 1,4,7 ; left
// 2,5,8,0 ; 가까운거, 같을시 왼손잡이냐 오른손잡이냐
// 3,6,9 ; right
switch (hand) {
case 'left':
hand = 'L';
break;
default:
hand = 'R';
break;
}
let curleft = '*';
let curright = '#';
let answer = '';
const maptable = {
1: { low: 0, col: 0 },
2: { low: 0, col: 1 },
3: { low: 0, col: 2 },
4: { low: 1, col: 0 },
5: { low: 1, col: 1 },
6: { low: 1, col: 2 },
7: { low: 2, col: 0 },
8: { low: 2, col: 1 },
9: { low: 2, col: 2 },
'*': { low: 3, col: 0 },
0: { low: 3, col: 1 },
'#': { low: 3, col: 2 },
};
let keypad = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
[-1, 0, -2],
];
function getCost(num, cur) {
let cost = 0;
const src = maptable[cur];
const des = maptable[num];
let low = Math.abs(des['low'] - src['low']);
let col = Math.abs(des['col'] - src['col']);
cost = low + col;
return cost;
}
let x;
numbers.forEach((number, idx) => {
if (number.toString().match(/[369]/)) {
x = 'R';
curright = number;
} else if (number.toString().match(/[147]/)) {
x = 'L';
curleft = number;
} else {
// 가까운손
const costleft = getCost(number, curleft);
const costright = getCost(number, curright);
if (costleft == costright) {
x = hand;
switch (hand) {
case 'L':
curleft = number;
break;
default:
curright = number;
break;
}
} else if (costleft > costright) {
x = 'R';
curright = number;
} else {
x = 'L';
curleft = number;
}
}
answer += x;
});
return answer;
}
result = solution([1, 3, 4, 5, 8, 2, 1, 4, 5, 9, 5], 'right');
result = solution([7, 0, 8, 2, 8, 3, 1, 5, 7, 6, 2], 'left');
result = solution([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], 'right');

해설

  • 입력으로 키패드를 누르는 순서와 왼손잡이인지 오른손잡이인지 알려주는 문자열을 받는다
  • 그러면 “LRLLLRLLRRL” 식으로 각 숫자를 어떤 손으로 눌렀는지 문자열로 반환한다
  • 147, 369는 좌우 키패드이기때문에 어떤 손으로 눌러야하는 정해져 있고,
  • 문제는 가운데 있는 키들이다. 2580
  • 2580중 하나의 키를 눌러야하는 상황이면 가까운 손으로 눌러야한다
  • 거리가 같은 경우에는 왼손잡이는 왼손으로, 오른손잡이는 오른손으로 누른다
  • 나는 정규식으로 147, 369를 처리하고
  • 가운데 키를 누르는 경우에는 왼손 오른손 누가더 가까운데 getCost 함수로 비교한다
  • getCost함수는 그냥 몇칸 떨어져 있는지 비교하는 단순한 함수이다
  • maptable이라는걸 만들어서 키패드를 2차원 배열로 표시해을 때 인덱스를 저장했다

핸드폰 번호 가리기

핸드폰 번호 가리기
1
2
3
4
5
6
7
8
9
10
// https://programmers.co.kr/learn/courses/30/lessons/12948
function solution(phone_number) {
let answer = '';
for (let index = 0; index < phone_number.length - 4; index++) {
answer += '*';
}
return answer + phone_number.slice(-4);
}

result = solution('01033334444');

해설

  • 인풋 문자열에서 뒤 4자리만 남기고 '*'문자로 변환하는 거다
  • 나는 그냥 인풋 문자열 길이에서 4번 적게 '*'을 찍고
  • 마지막 문자열 4개를 잘라다가 붙였다

다른 사람의 풀이

1
2
3
4
// 정규식 풀이 다른사람
function hide_numbers(s) {
return s.replace(/\d(?=\d{4})/g, '*');
}
  • 정규식을 이용해 아름답게 줄였다..

참고 정규식 시각화하는 곳