Skip to content

ReDoS - Regular Expression Denial of Service

Hưởng ứng phong trào viết lách của anh em trong nhóm, hôm nay mình sẽ viết một bài liên quan đến Regular Expression, RegExp, hay Regex - Một công cụ mà chắc là 100% anh em DEV đều đã từng sử dụng. 😛

Hẳn ai cũng đều rõ lạm dụng Regex có thể gây ảnh hưởng xấu đến hiệu suất ứng dụng, đặc biệt là các biểu thức phức tạp. Nhưng bạn đã bao giờ nghe về việc lợi dụng Regex để tấn công từ chối dịch vụ (Regular Expression Denial of Service - ReDoS) chưa? Trong bài viết này, mình sẽ giới thiệu về ReDoS và các cách phòng tránh cơ bản.

Sử dụng Regex không cẩn thận

Đầu tiên, giả sử mình có một ứng dụng NodeJS như sau:

const express = require('express');
const app = express();

app.get('/api', (req, res) => {
  const regex = /^(a+)+$/;
  const input = req.query.input;
  if (regex.test(input)) {
    res.send('Matched');
  } else {
    res.send('Not matched');
  }
});

app.listen(8000)

Ứng dụng này đơn giản là kiểm tra chuỗi input trên query string có phải là 1 chuỗi các ký tự a liên tiếp hay không.

Chạy thử:

$ curl 'http://localhost:8000/api?input=aaaab'
Not matched
$ curl 'http://localhost:8000/api?input=aaaa'
Matched

Bạn có thể thấy rằng ứng dụng hoạt động đúng như mong đợi!

Nhưng hãy thử trường hợp sau với 32 kí tự a liên tiếp:

$ curl 'http://localhost:8000/api?input=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
Matched

Vẫn ổn, giờ hãy đổi kí tự cuối cùng thành !

$ curl 'http://localhost:8000/api?input=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'
...

Oops! Ứng dụng của bạn bị treo! Các request tiếp theo dù có input ngắn hơn, cũng sẽ không được xử lý. Đây chính là một cách tấn công ReDoS.

Vấn đề nằm ở biểu thức /(a+)+$/, vốn thừa thãi một cách không cần thiết. Trong đó:

Bạn có thể hình dung biểu thức này tương đương một thuật toán với độ phức tạp O(2^n), trong đó n là dộ dài chuỗi a. Như vậy, với chuỗi aaaa bạn có 16 trường hợp; thì với chuỗi aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa! bạn có đến 2^32 = 4.294.967.296 trường hợp.

Và do chuỗi input không khớp với mẫu, biểu thức này phải kiểm tra hết toàn bộ các trường hợp có thể. Nếu con số này quá lớn sẽ làm ứng dụng của bạn bị treo.

Để fix chỗ này thì chỉ cần sửa lại thành /^a+$/, khi này biểu thức sẽ chỉ kiểm tra một lượt các ký tự trong input.

Một vài mẫu đơn giản khác bạn có thể thử:

Bị khai thác từ bên ngoài

Có thể bạn sẽ thấy ví dụ trên không thực tế, tôi sẽ không viết một cái Regex như vậy. Nhưng đừng quên rằng, ứng dụng của bạn cũng có thể bị khai thác từ bên ngoài!

Giả sử bạn có một trang web với chức năng đăng bài và tìm kiếm, vd đơn giản như sau:

const express = require('express');
const app = express();

app.post('/posts', async (req, res) => {
  const post = req.body;
  await Post.create(post);
  res.send('OK');
});

app.get('/posts/search', async (req, res) => {
  const regex = new RegExp(req.query.q, 'i');
  const posts = await Post.find({ title: regex });
  res.json(posts);
});

app.listen(8000)

Nếu kẻ xấu tạo một bài post với tiêu đề aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa! trước, sau đó tìm kiếm với từ khóa (a+)+$ thì ứng dụng của bạn cũng sẽ bị treo.

Phòng tránh

Để phòng tránh bị ReDoS, bạn có thể thực hiện một số cách sau:

  1. Sử dụng biểu thức Regex đơn giản
  1. Sử dụng thư viện hỗ trợ
  1. Không tin tưởng người dùng: Một nguyên tắc cơ bản, không bao giờ tin tưởng người dùng. Đặc biệt là với dữ liệu người dùng nhập vào. Hãy luôn kiểm tra và xử lý dữ liệu đầu vào một cách cẩn thận từ độ dài đến nội dung bên trong. Chỉ cho phép tối thiểu những kí tự cần thiết.

  2. Thiết lập thời gian timeout: Nếu ứng dụng của bạn cần phải cho người dùng nhập Regex tùy chỉnh, hãy đặt thời gian timeout cho việc kiểm tra Regex, nếu vượt quá thời gian quy định thì process cần được hủy bỏ.


Hy vọng bài viết này giúp bạn hiểu hơn về Regex và các mối nguy tiềm ẩn khi sử dụng công cụ này.

Happy coding! 🧑🏻‍💻

References