ในแอปพลิเคชันเซิร์ฟเวอร์ หนึ่งในตัวบ่งชี้ที่สำคัญที่สุดคือความปลอดภัย นี่เป็นหนึ่ง ใน ประเภทของNon-Functional Requirements
การรักษาความปลอดภัยมีองค์ประกอบหลายอย่าง แน่นอนว่าเพื่อที่จะครอบคลุมหลักการและการดำเนินการในการป้องกันทั้งหมดที่ทราบโดยสมบูรณ์ คุณต้องเขียนบทความมากกว่าหนึ่งบทความ ดังนั้นเรามาเน้นที่สิ่งที่สำคัญที่สุดกันดีกว่า บุคคลที่รอบรู้ในหัวข้อนี้เป็นอย่างดีจะสามารถตั้งค่ากระบวนการทั้งหมดและให้แน่ใจว่าพวกเขาไม่ได้สร้างช่องโหว่ด้านความปลอดภัยใหม่ ซึ่งเป็นสิ่งจำเป็นในทีมใดๆ แน่นอนว่าคุณไม่ควรคิดว่าหากคุณปฏิบัติตามแนวทางปฏิบัติเหล่านี้ ใบสมัครจะปลอดภัยอย่างสมบูรณ์ เลขที่! แต่มันจะปลอดภัยกว่ากับพวกเขาอย่างแน่นอน ไป.
นอกจากนี้ ดังที่คุณเห็นจากภาพด้านบน หากเวอร์ชันใหม่มีวิธีแก้ไขช่องโหว่นี้ Snyk จะเสนอให้ดำเนินการดังกล่าวและสร้าง Pull-Request สามารถใช้งานได้ฟรีสำหรับโครงการโอเพ่นซอร์ส โปรเจ็กต์จะถูกสแกนด้วยความถี่: สัปดาห์ละครั้ง เดือนละครั้ง ฉันลงทะเบียนและเพิ่มที่เก็บข้อมูลสาธารณะทั้งหมดของฉันไปที่ Snyk scan (ไม่มีอะไรที่เป็นอันตรายเกี่ยวกับเรื่องนี้: ทุกคนเปิดอยู่แล้ว) ถัดไป Snyk แสดงผลลัพธ์ของการสแกน:
และหลังจากนั้นไม่นาน Snyk-bot ได้เตรียม Pull-Requests หลายรายการในโครงการที่จำเป็นต้องอัปเดตการพึ่งพา:
และนี่คืออีกอย่าง:
ดังนั้นนี่คือเครื่องมือที่ยอดเยี่ยมสำหรับการค้นหาช่องโหว่และการตรวจสอบเพื่ออัปเดต เวอร์ชันใหม่
ในคำพูดภาษาอังกฤษ วลี “ข้อมูลที่ละเอียดอ่อน” เป็นเรื่องปกติมากกว่า การเปิดเผยข้อมูลส่วนบุคคล หมายเลขบัตรเครดิต และข้อมูลส่วนบุคคลอื่น ๆ ของลูกค้าอาจทำให้เกิดอันตรายที่แก้ไขไม่ได้ ก่อนอื่น คุณต้องพิจารณาการออกแบบแอปพลิเคชันอย่างละเอียด และพิจารณาว่าจำเป็นต้องใช้ข้อมูลใดๆ จริงหรือไม่ บางทีอาจจะไม่จำเป็นสำหรับบางคนแต่ก็เสริมไว้เพื่ออนาคตที่ยังมาไม่ถึงและไม่น่าจะมา นอกจากนี้ ในระหว่างการบันทึกโครงการ ข้อมูลดังกล่าวอาจรั่วไหล วิธีง่ายๆ ในการป้องกันไม่ให้ข้อมูลที่ละเอียดอ่อนเข้าไปในบันทึกของคุณคือการล้างวิธีการ
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 เป็นต้น



ใช้แล็บความปลอดภัย 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