ในแอปพลิเคชันเซิร์ฟเวอร์ หนึ่งในตัวบ่งชี้ที่สำคัญที่สุดคือความปลอดภัย นี่เป็นหนึ่ง ใน ประเภทของNon-Functional Requirements การรักษาความปลอดภัยมีองค์ประกอบหลายอย่าง แน่นอนว่าเพื่อที่จะครอบคลุมหลักการและการดำเนินการในการป้องกันทั้งหมดที่ทราบโดยสมบูรณ์ คุณต้องเขียนบทความมากกว่าหนึ่งบทความ ดังนั้นเรามาเน้นที่สิ่งที่สำคัญที่สุดกันดีกว่า บุคคลที่รอบรู้ในหัวข้อนี้เป็นอย่างดีจะสามารถตั้งค่ากระบวนการทั้งหมดและให้แน่ใจว่าพวกเขาไม่ได้สร้างช่องโหว่ด้านความปลอดภัยใหม่ ซึ่งเป็นสิ่งจำเป็นในทีมใดๆ แน่นอนว่าคุณไม่ควรคิดว่าหากคุณปฏิบัติตามแนวทางปฏิบัติเหล่านี้ ใบสมัครจะปลอดภัยอย่างสมบูรณ์ เลขที่! แต่มันจะปลอดภัยกว่ากับพวกเขาอย่างแน่นอน ไป.
1. ให้ความปลอดภัยในระดับภาษา Java
ประการแรก การรักษาความปลอดภัยใน Java เริ่มต้นที่ระดับคุณลักษณะของภาษา นี่คือสิ่งที่เราจะทำหากไม่มีตัวดัดแปลงการเข้าถึง... อนาธิปไตยไม่น้อย ภาษาการเขียนโปรแกรมช่วยให้เราเขียนโค้ดที่ปลอดภัย และยังใช้ประโยชน์จากคุณลักษณะด้านความปลอดภัยโดยนัยอีกมากมาย:- การพิมพ์ที่แข็งแกร่ง Javaเป็นภาษาที่พิมพ์แบบคงที่ซึ่งให้ความสามารถในการตรวจจับข้อผิดพลาดของประเภทในขณะรันไทม์
- ตัวดัดแปลงการเข้าถึง ขอบคุณพวกเขา เราจึงสามารถกำหนดค่าการเข้าถึงคลาส วิธีการ และฟิลด์คลาสได้ตามที่เราต้องการ
- การจัดการหน่วยความจำอัตโนมัติ เพื่อจุดประสงค์นี้ เรา (Javaists ;)) มี Garbage Collector ซึ่งช่วยให้คุณไม่ต้องกำหนดค่าด้วยตนเอง ใช่ บางครั้งปัญหาก็เกิดขึ้น
- การตรวจสอบ Bytecode: Java คอมไพล์เป็น bytecode ซึ่งตรวจสอบโดยรันไทม์ก่อนที่จะรัน
- หลีกเลี่ยงการทำให้คลาสที่มีความสำคัญด้านความปลอดภัยเป็นอนุกรม ในกรณีนี้ คุณสามารถรับคลาสอินเตอร์เฟสจากไฟล์ซีเรียลไลซ์ ไม่ต้องพูดถึงข้อมูลที่กำลังซีเรียลไลซ์
- พยายามหลีกเลี่ยงคลาสข้อมูลที่ไม่แน่นอน สิ่งนี้ให้ประโยชน์ทั้งหมดของคลาสที่ไม่เปลี่ยนรูป (เช่น ความปลอดภัยของเธรด) หากมีวัตถุที่ไม่แน่นอน สิ่งนี้อาจทำให้เกิดลักษณะการทำงานที่ไม่คาดคิด
- ทำสำเนาของอ็อบเจ็กต์ที่ไม่แน่นอนที่ส่งคืน หากวิธีการส่งคืนการอ้างอิงไปยังวัตถุที่ไม่แน่นอนภายใน รหัสไคลเอนต์จะสามารถเปลี่ยนสถานะภายในของวัตถุได้
- และอื่นๆ...
2. กำจัดช่องโหว่ของการแทรก SQL
ช่องโหว่ที่ไม่ซ้ำใคร ความเป็นเอกลักษณ์ของมันอยู่ที่ความจริงที่ว่ามันเป็นทั้งช่องโหว่ที่มีชื่อเสียงที่สุดและเป็นหนึ่งในช่องโหว่ที่พบบ่อยที่สุด หากคุณไม่สนใจเรื่องความปลอดภัยคุณก็คงจะไม่รู้เรื่องนี้ SQL Inject คืออะไร? นี่คือการโจมตีฐานข้อมูลโดยการฉีดโค้ด SQL เพิ่มเติมในตำแหน่งที่ไม่คาดหวัง สมมติว่าเรามีวิธีการที่รับพารามิเตอร์บางอย่างในการสืบค้นฐานข้อมูล ตัวอย่างเช่น ชื่อผู้ใช้ รหัสที่มีช่องโหว่จะมีลักษณะดังนี้:// Метод достает из базы данных всех пользователей с определенным именем
public List<User> findByFirstName(String firstName) throws SQLException {
// Создается связь с базой данных
Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);
// Пишем sql request в базу данных с нашим firstName
String query = "SELECT * FROM USERS WHERE firstName = " + firstName;
// выполняем request
Statement statement = connection.createStatement();
ResultSet result = statement.executeQuery(query);
// при помощи mapToUsers переводит ResultSet в коллекцию юзеров.
return mapToUsers(result);
}
private List<User> mapToUsers(ResultSet resultSet) {
//переводит в коллекцию юзеров
}
ในตัวอย่างนี้ แบบสอบถาม sql จะถูกจัดเตรียมไว้ล่วงหน้าในบรรทัดที่แยกจากกัน ดูเหมือนว่าปัญหาคืออะไรใช่ไหม? บางทีปัญหาคือมันจะดีกว่าที่จะใช้String.format
? เลขที่? แล้วไงล่ะ? ลองมาแทนที่เราในฐานะผู้ทดสอบและคิดถึงสิ่งที่สามารถสื่อความหมายได้ในค่าfirstName
นี้ ตัวอย่างเช่น:
- คุณสามารถผ่านสิ่งที่คาดหวังได้ - ชื่อผู้ใช้ จากนั้นฐานข้อมูลจะส่งคืนผู้ใช้ทั้งหมดที่มีชื่อนั้น
- คุณสามารถส่งสตริงว่างได้: จากนั้นผู้ใช้ทั้งหมดจะถูกส่งคืน
- หรือคุณสามารถผ่านสิ่งต่อไปนี้: “''; วางผู้ใช้ตาราง;” และที่นี่จะมีปัญหาใหญ่กว่านี้ แบบสอบถามนี้จะลบตารางออกจากฐานข้อมูล ด้วยข้อมูลทั้งหมด ทุกคน.
// Метод достает из базы данных всех пользователей с определенным именем
public List<User> findByFirstName(String firstName) throws SQLException {
// Создается связь с базой данных
Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);
// Создаем параметризированный request.
String query = "SELECT * FROM USERS WHERE firstName = ?";
// Создаем подготовленный стейтмент с параметризованным requestом
PreparedStatement statement = connection.prepareStatement(query);
// Передаем meaning параметра
statement.setString(1, firstName);
// выполняем request
ResultSet result = statement.executeQuery(query);
// при помощи mapToUsers переводим ResultSet в коллекцию юзеров.
return mapToUsers(result);
}
private List<User> mapToUsers(ResultSet resultSet) {
//переводим в коллекцию юзеров
}
ด้วยวิธีนี้จะหลีกเลี่ยงช่องโหว่นี้ สำหรับผู้ที่ต้องการเจาะลึกประเด็นนี้มากกว่าบทความนี้นี่คือตัวอย่างที่ดี จะรู้ได้อย่างไรว่าเข้าใจส่วนนี้? หากเรื่องตลกด้านล่างนี้ชัดเจน แสดงว่านี่คือสัญญาณที่ชัดเจนว่าแก่นแท้ของช่องโหว่นั้นชัดเจน :D
3. สแกนและอัปเดตการอ้างอิงอยู่เสมอ
มันหมายความว่าอะไร? สำหรับผู้ที่ไม่รู้ว่าการพึ่งพาคืออะไร ฉันจะอธิบาย: มันเป็นไฟล์เก็บถาวร jar ที่มีโค้ดที่เชื่อมต่อกับโปรเจ็กต์โดยใช้ระบบบิลด์อัตโนมัติ (Maven, Gradle, Ant) เพื่อนำโซลูชันของผู้อื่นกลับมาใช้ใหม่ ตัวอย่างเช่นProject Lombokซึ่งสร้าง getters, setters ฯลฯ สำหรับเราในขณะรันไทม์ และถ้าเราพูดถึงแอปพลิเคชันขนาดใหญ่ แอปพลิเคชันเหล่านั้นใช้การขึ้นต่อกันที่แตกต่างกันมากมาย บางส่วนเป็นแบบสกรรมกริยา (นั่นคือ แต่ละการขึ้นต่อกันสามารถมีการขึ้นต่อกันของตัวเองได้ และอื่นๆ) ดังนั้น ผู้โจมตีจึงให้ความสำคัญกับการพึ่งพาโอเพ่นซอร์สมากขึ้น เนื่องจากมีการใช้งานเป็นประจำและอาจทำให้เกิดปัญหากับไคลเอนต์จำนวนมากได้ สิ่งสำคัญคือต้องตรวจสอบให้แน่ใจว่าไม่มีช่องโหว่ที่ทราบในแผนผังการขึ้นต่อกันทั้งหมด (ซึ่งก็คือลักษณะที่ปรากฏ) และมีหลายวิธีในการทำเช่นนี้ใช้ Snyk ในการตรวจสอบ
เครื่องมือSnykจะตรวจสอบการขึ้นต่อกันของโปรเจ็กต์ทั้งหมดและตั้งค่าสถานะช่องโหว่ที่ทราบ ที่นั่นคุณสามารถลงทะเบียนและนำเข้าโปรเจ็กต์ของคุณผ่าน GitHub เป็นต้น นอกจากนี้ ดังที่คุณเห็นจากภาพด้านบน หากเวอร์ชันใหม่มีวิธีแก้ไขช่องโหว่นี้ Snyk จะเสนอให้ดำเนินการดังกล่าวและสร้าง Pull-Request สามารถใช้งานได้ฟรีสำหรับโครงการโอเพ่นซอร์ส โปรเจ็กต์จะถูกสแกนด้วยความถี่: สัปดาห์ละครั้ง เดือนละครั้ง ฉันลงทะเบียนและเพิ่มที่เก็บข้อมูลสาธารณะทั้งหมดของฉันไปที่ Snyk scan (ไม่มีอะไรที่เป็นอันตรายเกี่ยวกับเรื่องนี้: ทุกคนเปิดอยู่แล้ว) ถัดไป Snyk แสดงผลลัพธ์ของการสแกน: และหลังจากนั้นไม่นาน Snyk-bot ได้เตรียม Pull-Requests หลายรายการในโครงการที่จำเป็นต้องอัปเดตการพึ่งพา: และนี่คืออีกอย่าง: ดังนั้นนี่คือเครื่องมือที่ยอดเยี่ยมสำหรับการค้นหาช่องโหว่และการตรวจสอบเพื่ออัปเดต เวอร์ชันใหม่ใช้แล็บความปลอดภัย GitHub
ผู้ที่ทำงานบน GitHub ยังสามารถใช้ประโยชน์จากเครื่องมือในตัวได้อีกด้วย คุณสามารถอ่านเพิ่มเติมเกี่ยวกับวิธีการ นี้ได้ในการแปลของฉันจากบล็อกประกาศของ GitHub Security Lab แน่นอนว่าเครื่องมือนี้ง่ายกว่า Snyk แต่คุณไม่ควรละเลยมันอย่างแน่นอน นอกจากนี้ จำนวนช่องโหว่ที่ทราบจะเพิ่มขึ้นเท่านั้น ดังนั้นทั้ง Snyk และ GitHub Security Lab จะขยายและปรับปรุงเปิดใช้งาน Sonatype DepShield
หากคุณใช้ GitHub เพื่อจัดเก็บพื้นที่เก็บข้อมูล คุณสามารถเพิ่มแอปพลิเคชันใดแอปพลิเคชันหนึ่งลงในโปรเจ็กต์ของคุณได้จาก MarketPlace - Sonatype DepShield ด้วยความช่วยเหลือนี้ คุณยังสามารถสแกนโปรเจ็กต์สำหรับการขึ้นต่อกันได้อีกด้วย นอกจากนี้ หากพบบางสิ่ง ปัญหา GitHub จะถูกสร้างขึ้นพร้อมคำอธิบายที่เกี่ยวข้อง ดังที่แสดงด้านล่าง:4. จัดการข้อมูลที่ละเอียดอ่อนด้วยความระมัดระวัง
ในคำพูดภาษาอังกฤษ วลี “ข้อมูลที่ละเอียดอ่อน” เป็นเรื่องปกติมากกว่า การเปิดเผยข้อมูลส่วนบุคคล หมายเลขบัตรเครดิต และข้อมูลส่วนบุคคลอื่น ๆ ของลูกค้าอาจทำให้เกิดอันตรายที่แก้ไขไม่ได้ ก่อนอื่น คุณต้องพิจารณาการออกแบบแอปพลิเคชันอย่างละเอียด และพิจารณาว่าจำเป็นต้องใช้ข้อมูลใดๆ จริงหรือไม่ บางทีอาจจะไม่จำเป็นสำหรับบางคนแต่ก็เสริมไว้เพื่ออนาคตที่ยังมาไม่ถึงและไม่น่าจะมา นอกจากนี้ ในระหว่างการบันทึกโครงการ ข้อมูลดังกล่าวอาจรั่วไหล วิธีง่ายๆ ในการป้องกันไม่ให้ข้อมูลที่ละเอียดอ่อนเข้าไปในบันทึกของคุณคือการล้างวิธีการtoString()
ของเอนทิตีโดเมน (เช่น ผู้ใช้ นักเรียน ครู และอื่นๆ) วิธีนี้จะป้องกันไม่ให้ช่องที่ละเอียดอ่อนถูกพิมพ์โดยไม่ได้ตั้งใจ หากคุณใช้ลอมบอกเพื่อสร้างเมธอดtoString()
คุณสามารถใช้คำอธิบายประกอบ@ToString.Exclude
เพื่อป้องกันไม่ให้มีการใช้ฟิลด์ในเอาท์พุตผ่านเมธอดtoString()
ได้ นอกจากนี้ ควรระมัดระวังในการแบ่งปันข้อมูลกับโลกภายนอกด้วย ตัวอย่างเช่น มีจุดสิ้นสุด http ที่แสดงชื่อของผู้ใช้ทั้งหมด ไม่จำเป็นต้องแสดง ID เฉพาะภายในของผู้ใช้ ทำไม เนื่องจากการใช้มัน ผู้โจมตีสามารถรับข้อมูลอื่นที่เป็นความลับมากขึ้นเกี่ยวกับผู้ใช้แต่ละคนได้ ตัวอย่างเช่น หากคุณใช้ Jackson เพื่อทำให้POJO เป็นอนุกรมและดีซีเรียลไลซ์ ลงในJSONคุณสามารถใช้@JsonIgnore
และ คำอธิบายประกอบ @JsonIgnoreProperties
เพื่อป้องกันไม่ให้ช่องใดช่องหนึ่งเป็นซีเรียลไลซ์และดีซีเรียลไลซ์ได้ โดยทั่วไป คุณต้องใช้คลาส POJO ที่แตกต่างกันสำหรับตำแหน่งที่ต่างกัน มันหมายความว่าอะไร?
- หากต้องการทำงานกับฐานข้อมูล ให้ใช้ POJO - Entity เท่านั้น
- หากต้องการทำงานกับตรรกะทางธุรกิจ ให้โอนเอนทิตีไปยังโมเดล
- หากต้องการทำงานกับโลกภายนอกและส่งคำขอ http ให้ใช้เอนทิตีที่สาม - DTO
ใช้อัลกอริธึมการเข้ารหัสและการแฮชที่แข็งแกร่ง
ข้อมูลลูกค้าที่เป็นความลับจะต้องได้รับการจัดเก็บอย่างปลอดภัย ในการทำเช่นนี้คุณต้องใช้การเข้ารหัส คุณต้องตัดสินใจว่าจะใช้การเข้ารหัสประเภทใด ทั้งนี้ขึ้นอยู่กับงาน นอกจากนี้ การเข้ารหัสที่แข็งแกร่งยิ่งขึ้นยังต้องใช้เวลามากขึ้น ดังนั้นคุณต้องพิจารณาอีกครั้งว่าความจำเป็นในการเข้ารหัสนั้นมากเพียงใดจึงเหมาะสมกับเวลาที่ใช้ไป แน่นอนคุณสามารถเขียนอัลกอริทึมได้ด้วยตัวเอง แต่นี่ไม่จำเป็น คุณสามารถใช้ประโยชน์จากโซลูชันที่มีอยู่ในพื้นที่นี้ได้ ตัวอย่างเช่นGoogle Tink :<!-- https://mvnrepository.com/artifact/com.google.crypto.tink/tink -->
<dependency>
<groupId>com.google.crypto.tink</groupId>
<artifactId>tink</artifactId>
<version>1.3.0</version>
</dependency>
มาดูวิธีใช้งานโดยใช้ตัวอย่างวิธีเข้ารหัสทางเดียวและอีกทางหนึ่ง:
private static void encryptDecryptExample() {
AeadConfig.register();
KeysetHandle handle = KeysetHandle.generateNew(AeadKeyTemplates.AES128_CTR_HMAC_SHA256);
String plaintext = "Цой жив!";
String aad = "Юрий Клинских";
Aead aead = handle.getPrimitive(Aead.class);
byte[] encrypted = aead.encrypt(plaintext.getBytes(), aad.getBytes());
String encryptedString = Base64.getEncoder().encodeToString(encrypted);
System.out.println(encryptedString);
byte[] decrypted = aead.decrypt(Base64.getDecoder().decode(encrypted), aad.getBytes());
System.out.println(new String(decrypted));
}
การเข้ารหัสรหัสผ่าน
สำหรับงานนี้ การใช้การเข้ารหัสแบบอสมมาตรจะปลอดภัยที่สุด ทำไม เพราะแอปพลิเคชันไม่จำเป็นต้องถอดรหัสรหัสผ่านกลับจริงๆ นี่คือแนวทางทั่วไป ในความเป็นจริง เมื่อผู้ใช้ป้อนรหัสผ่าน ระบบจะเข้ารหัสและเปรียบเทียบกับรหัสผ่านที่อยู่ในตู้นิรภัย การเข้ารหัสจะดำเนินการโดยใช้วิธีการเดียวกัน ดังนั้นคุณสามารถคาดหวังได้ว่ามันจะตรงกัน (ถ้าคุณป้อนรหัสผ่านที่ถูกต้อง ;) แน่นอน) BCrypt และ SCrypt เหมาะสำหรับจุดประสงค์นี้ ทั้งสองเป็นฟังก์ชันทางเดียว (แฮชการเข้ารหัส) ที่มีอัลกอริธึมที่ซับซ้อนในการคำนวณซึ่งใช้เวลานาน นี่คือสิ่งที่คุณต้องการเนื่องจากการถอดรหัสโดยตรงจะใช้เวลาตลอดไป ตัวอย่างเช่น Spring Security รองรับอัลกอริธึมที่หลากหลายSCryptPasswordEncoder
คุณ ยัง สามารถ ใช้BCryptPasswordEncoder
. อัลกอริธึมการเข้ารหัสที่แข็งแกร่งตอนนี้คืออะไรอาจจะอ่อนแอในปีหน้า ด้วยเหตุนี้เราจึงสรุปได้ว่าจำเป็นต้องตรวจสอบอัลกอริธึมที่ใช้และอัปเดตไลบรารีด้วยอัลกอริธึม
แทนที่จะเป็นเอาท์พุต
วันนี้เราคุยกันเรื่องความปลอดภัย และแน่นอนว่ายังมีอีกหลายสิ่งที่ทิ้งไว้เบื้องหลัง ฉันเพิ่งเปิดประตูสู่โลกใหม่สำหรับคุณ: โลกที่มีชีวิตของตัวเอง ในเรื่องความปลอดภัย มันก็เหมือนกับเรื่องการเมือง: ถ้าคุณไม่ยุ่งการเมือง การเมืองก็จะยุ่งกับคุณ ตามเนื้อผ้า ฉันแนะนำให้สมัครบัญชี Github ของฉัน ที่นั่นฉันโพสต์ผลงานเกี่ยวกับเทคโนโลยีต่างๆ ที่ฉันศึกษาและนำไปใช้ในที่ทำงานลิงค์ที่เป็นประโยชน์
ใช่ บทความเกือบทั้งหมดบนเว็บไซต์เขียนเป็นภาษาอังกฤษ ไม่ว่าเราจะชอบหรือไม่ก็ตาม ภาษาอังกฤษเป็นภาษาที่โปรแกรมเมอร์ใช้สื่อสาร บทความ หนังสือ และนิตยสารใหม่ล่าสุดเกี่ยวกับการเขียนโปรแกรมเขียนเป็นภาษาอังกฤษ นั่นเป็นสาเหตุที่ลิงก์ไปยังคำแนะนำของฉันส่วนใหญ่เป็นภาษาอังกฤษ:- Habr: การฉีด SQL สำหรับผู้เริ่มต้น
- ออราเคิล: ศูนย์ทรัพยากรความปลอดภัย Java
- Oracle: แนวทางการเข้ารหัสที่ปลอดภัยสำหรับ Java SE
- Baeldung: พื้นฐานของการรักษาความปลอดภัยของ Java
- ปานกลาง: 10 เคล็ดลับในการเพิ่มพลังความปลอดภัย Java ของคุณ
- Snyk: แนวทางปฏิบัติที่ดีที่สุดด้านความปลอดภัย 10 ประการของ Java
- JR: ประกาศของ GitHub Security Lab: ปกป้องโค้ดทั้งหมดของคุณร่วมกัน
GO TO FULL VERSION