Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> สมาร์ทโฟน >> iPhone

การพัฒนา iOS ที่ปลอดภัยด้วย Swift: หลีกเลี่ยงข้อผิดพลาดทั่วไปและเสริมสร้างความปลอดภัยให้แอปของคุณ

ในปัจจุบัน มีหลายวิธีที่ผู้โจมตีสามารถพยายามเจาะระบบแอปพลิเคชันของคุณได้ และเนื่องจากอาชญากรรมทางไซเบอร์เพิ่มขึ้นอย่างต่อเนื่อง ความต้องการสำหรับแอปพลิเคชันมือถือที่ปลอดภัย - และโดยปริยายคือการเขียนโค้ดอย่างปลอดภัย - จึงไม่เคยสูงขึ้นมาก่อน

ดังนั้น หากคุณเป็นนักพัฒนา iOS คุณควรเรียนรู้ที่จะวางความสำคัญให้กับความปลอดภัยในทุกขั้นตอนของการพัฒนาแอป

Swift ภาษาโปรแกรมสมัยใหม่ของ Apple มีเครื่องมือและเฟรมเวิร์กมากมายที่ช่วยลดความซับซ้อนในการพัฒนา ในขณะเดียวกันก็เพิ่มความปลอดภัย แต่ก็ต่อเมื่อใช้งานอย่างถูกต้องเท่านั้น

บทความนี้จะสำรวจ 10 ช่องโหว่ด้านความปลอดภัยที่พบบ่อยในแอป iOS ที่พัฒนาด้วย Swift และนำเสนอกลยุทธ์ที่เป็นประโยชน์เพื่อลดความเสี่ยงเหล่านั้น

ข้อกำหนดเบื้องต้น

ก่อนจะเริ่มต้น คุณจะต้องมี:

  • ความรู้ทำงานเกี่ยวกับ Swift และการพัฒนา iOS

  • การเข้าถึง Xcode

  • ความเข้าใจพื้นฐานเกี่ยวกับวิธีที่แอป iOS ติดต่อกับเซิร์ฟเวอร์

  • ความคุ้นเคยกับพื้นฐาน Terminal/คำสั่งบรรทัดคำสั่ง

ตัวอย่างโค้ดเป็นแบบปฏิบัติจริงและอธิบายทีละขั้นตอน ทำให้เข้าถึงได้สำหรับนักพัฒนารุ่นใหม่ ในขณะที่ยังมีคุณค่าสำหรับนักพัฒนาที่มีประสบการณ์ที่ต้องการเสริมสร้างความปลอดภัยของแอป

สิ่งที่เราจะครอบคลุม:

  • ช่องโหว่ด้านความปลอดภัยที่พบบ่อยที่สุดในแอปพลิเคชัน iOS ที่ใช้ Swift คืออะไร?
    • 1. การจัดเก็บข้อมูลที่ไม่ปลอดภัย
    • 2. การสื่อสารเครือข่ายที่อ่อนแอ
    • 3. การตรวจสอบความถูกต้องของอินพุตที่ไม่เหมาะสม
    • 4. การฮาร์ดโค้ดข้อมูลลับ
    • 5. การตรวจสอบสิทธิ์และการอนุญาตที่ไม่เพียงพอ
    • 6. การบันทึกบันทึก (Logging) และการจัดการข้อผิดพลาดที่ไม่ปลอดภัย
    • 7. การละเลยการปิดบังโค้ดและการวิศวกรรมย้อนกลับ
    • 8. ไลบรารีของบุคคลที่สามที่ไม่ปลอดภัย
    • 9. การตรวจสอบสิทธิ์ชีวภาพและหลายปัจจัยที่ไม่เพียงพอ
    • 10. การไม่สนใจการทดสอบความปลอดภัยเป็นระยะ

ช่องโหว่ด้านความปลอดภัยที่พบบ่อยที่สุดในแอปพลิเคชัน iOS ที่ใช้ Swift คืออะไร?

Swift และ iOS มีคุณสมบัติด้านความปลอดภัยที่แข็งแกร่ง แต่ข้อผิดพลาดยังคงเกิดขึ้น ดังต่อไปนี้คือกับดักที่พบบ่อยที่สุดและวิธีแก้ไข:

1. การจัดเก็บข้อมูลที่ไม่ปลอดภัย

ข้อผิดพลาดที่พบบ่อยที่สุดของนักพัฒนาคือการจัดเก็บข้อมูลที่ละเอียดอ่อนอย่างไม่ปลอดภัย รหัสผ่าน โทเคน หรือแม้แต่อ้อมูลผู้ใช้รายบุคคลอาจถูกทิ้งไว้ใน UserDefaults หรือที่จัดเก็บในเครื่องแบบไม่เข้ารหัสโดยไม่ได้ตั้งใจ

ในขณะที่ UserDefaults สะดวกสำหรับข้อมูลปริมาณเล็ก แต่ไม่ปลอดภัยสำหรับข้อมูลที่ละเอียดอ่อน เนื่องจากเข้าถึงได้ง่ายโดยผู้โจมตีหากอุปกรณ์ถูกเจาะระบบ

วิธีแก้ไข:

ใช้ Keychain Services API เพื่อจัดเก็บข้อมูลที่ละเอียดอ่อนอย่างปลอดภัย Keychain เข้ารหัสข้อมูลและผูกมัดกับอุปกรณ์ ทำให้ไม่สามารถเข้าถึงได้โดยแอปพลิเคชันหรือผู้ใช้ที่ไม่ได้รับอนุญาต

คุณสามารถจัดเก็บข้อมูลรับรองอย่างปลอดภัยโดยใช้ไลบรารีใน Swift เช่น KeychainAccess หรือฟังก์ชัน SecItemAdd และ SecItemCopyMatching ที่สร้างอยู่ในตัว

ตัวอย่างเช่น นี่คือวิธีจัดเก็บรหัสผ่านผู้ใช้ใน Keychain เพื่อให้แน่ใจว่าข้อมูลที่ละเอียดอ่อนถูกจัดเก็บอย่างปลอดภัย:

do { try keychain.set("userPassword123", key: "userPassword") } catch { print("Error saving to Keychain: \(error)") }

เบื้องหลังเมื่อคุณเรียก keychain.set("userPassword123", key: "userPassword") ภายในคลาส KeychainManager ที่ใช้เฟรมเวิร์ก Security ของ Apple สำหรับการจัดเก็บ:

import Security class KeychainManager { func set(_ value: String, key: String) throws { // 1. แปลงสตริงเป็น Data guard let data = value.data(using: .utf8) else { throw NSError(domain: "KeychainManager", code: -1) } // 2. สร้างพจนานุกรมแบบสอบถาม let query: [String: Any] = [ // จัดเก็บเป็นรหัสผ่าน kSecClass as String: kSecClassGenericPassword, // Bundle identifier ของแอปคุณ kSecAttrService as String: "com.yourapp.keychain", // "userPassword" kSecAttrAccount as String: key, // "userPassword123" เข้ารหัส kSecValueData as String: data, kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock ] // 3. บันทึกลง Keychain (iOS เข้ารหัสโดยอัตโนมัติ) let status = SecItemAdd(query as CFDictionary, nil) // 4. ตรวจสอบว่าสำเร็จหรือไม่ guard status == errSecSuccess else { throw NSError(domain: "KeychainManager", code: Int(status)) } } }

เมื่อฟังก์ชันนี้ทำงาน iOS จะแปลงค่าสตริง เช่น "userPassword123" เป็นข้อมูลไบนารีที่เข้ารหัส และจัดเก็บอย่างปลอดภัยในฐานข้อมูล Keychain ของอุปกรณ์ รายการนี้ถูกบันทึกภายใต้คีย์ที่ระบุ (เช่น "userPassword") และเฉพาะแอปของคุณเท่านั้นที่สามารถเข้าถึงได้

เบื้องหลัง Keychain ใช้คุณสมบัติความปลอดภัยที่แข็งแกร่ง รวมถึงการเข้ารหัสที่รองรับด้วยฮาร์ดแวร์โดยใช้คีย์เฉพาะอุปกรณ์ การป้องกันด้วยชีวภาพที่เป็นทางเลือกผ่าน Face ID หรือ Touch ID และการแยกแอปในระดับแอปพลิเคชัน เพื่อให้แน่ใจว่าแอปอื่นไม่สามารถอ่านหรือแก้ไขข้อมูลรับรองที่จัดเก็บของคุณได้

2. การสื่อสารเครือข่ายที่อ่อนแอ

การส่งข้อมูลที่ละเอียดอ่อนผ่านเครือข่ายเป็นอีกพื้นที่ที่อ่อนไหวต่อช่องโหว่ การใช้การเชื่อมต่อ HTTP ที่ไม่เข้ารหัสทำให้แอปของคุณ 드러งต่อการโจมตีแบบ Man-in-the-middle (MITM) อนุญาตให้ผู้โจมตีดักจับและแก้ไขข้อมูลระหว่างการส่ง

ปัญหา: เมื่อข้อมูลเดินทางระหว่างแอปและเซิร์ฟเวอร์ผ่านการเชื่อมต่อที่ไม่ปลอดภัย ผู้โจมตีในเครือข่ายเดียวกัน (เช่น Wi-Fi สาธารณะ) สามารถ:

  • อ่านข้อมูลที่ละเอียดอ่อน (รหัสผ่าน ข้อมูลส่วนตัว รายละเอียดการชำระเงิน)

  • แก้ไขคำขอและการตอบสนอง

  • ปลอมแปลงเป็นเซิร์ฟเวอร์ที่ถูกต้องของคุณ

วิธีแก้ไข:

1. ใช้ HTTPS เสมอ
HTTPS เข้ารหัสข้อมูลทั้งหมดระหว่างการส่ง ทำให้ผู้โจมตีอ่านไม่ได้ App Transport Security (ATS) ของ iOS บังคับใช้สิ่งนี้โดยการบล็อกการเชื่อมต่อ HTTP ที่ไม่ปลอดภัยโดยค่าเริ่มต้น:

// ❌ ไม่ปลอดภัย - การเชื่อมต่อ HTTP (ถูกบล็อกโดย ATS โดยค่าเริ่มต้น) let url = URL(string: "https://api.example.com/login") // ✅ ปลอดภัย - การเชื่อมต่อ HTTPS let url = URL(string: "https://api.example.com/login")

คุณควรหลีกเลี่ยงการเพิ่มข้อยกเว้น ATS ใน Info.plist เว้นแต่จำเป็นอย่างยิ่ง หาก API ของบุคคลที่สามรองรับเฉพาะ HTTP ให้ติดต่อพวกเขาเพื่ออัปเกรดหรือหาทางเลือกที่ปลอดภัยกว่า

2. ใช้งาน Certificate Pinning (การป้องกันขั้นสูง)

แม้จะมี HTTPS แอปของคุณก็ยังอาจเปราะบางต่อการโจมตี MITM ที่ซับซ้อน ผู้โจมตีอาจติดตั้งใบรับรองปลอมบนอุปกรณ์ของผู้ใช้ (ผ่านมัลแวร์หรือวิศวกรรมสังคม) และดักจับการสื่อสาร HTTPS ที่ดูเหมือนถูกต้อง ใบรับรองปลอมของผู้โจมตีจะได้รับความไว้วางใจจากอุปกรณ์ อนุญาตให้ถอดรหัสและอ่านการสื่อสารที่ "ปลอดภัย"

Certificate pinning แก้ปัญหานี้โดยทำให้แอปของคุณไว้วางใจเฉพาะใบรับรองของเซิร์ฟเวอร์เฉพาะของคุณ ปฏิเสธใบรับรองอื่นทั้งหมด - แม้จะถูกต้องในทางอื่น

วิธีการทำงานของ Certificate Pinning:

แอปของคุณจัดเก็บใบรับรองที่คาดหวัง (หรือแฮชคีย์สาธารณะ) และตรวจสอบความถูกต้องในแต่ละการเชื่อมต่อ:

class SecureNetworkManager: NSObject, URLSessionDelegate { // จัดเก็บแฮชใบรับรองของเซิร์ฟเวอร์ // รับค่านี้โดยรัน: openssl x509 -in certificate.crt -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64 private let expectedPublicKeyHash = "YOUR_CERTIFICATE_HASH_HERE" func urlSession( _ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void ) { // ขั้นตอนที่ 1: ตรวจสอบว่านี้เป็นการท้าทายความไว้วางใจของเซิร์ฟเวอร์ (การตรวจสอบใบรับรอง) guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, let serverTrust = challenge.protectionSpace.serverTrust else { // ไม่ใช่การท้าทายใบรับรอง ใช้การจัดการเริ่มต้น completionHandler(.performDefaultHandling, nil) return } // ขั้นตอนที่ 2: ตรวจสอบว่าใบรับรองของเซิร์ฟเวอร์ตรงกับใบรับรองที่ผูกปิดไว้หรือไม่ if isValidServerTrust(serverTrust) { // ใบรับรองตรงกัน - ดำเนินการเชื่อมต่อต่อ completionHandler(.useCredential, URLCredential(trust: serverTrust)) } else { // ใบรับรองไม่ตรง - ปฏิเสธการเชื่อมต่อเพื่อป้องกันการโจมตี MITM completionHandler(.cancelAuthenticationChallenge, nil) } } // ตรวจสอบความถูกต้องของใบรับรองเซิร์ฟเวอร์กับแฮชที่ผูกปิดไว้ private func isValidServerTrust(_ serverTrust: SecTrust) -> Bool { // ดึงใบรับรองของเซิร์ฟเวอร์ guard let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else { return false } // รับคีย์สาธารณะจากใบรับรอง let serverPublicKey = SecCertificateCopyKey(serverCertificate) guard let publicKey = serverPublicKey else { return false } // แปลงคีย์สาธารณะเป็นข้อมูลและแฮช var error: Unmanaged<CFError>? guard let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, &error) as Data? else { return false } // แฮชคีย์สาธารณะโดยใช้ SHA-256 let publicKeyHash = SHA256.hash(data: publicKeyData) let publicKeyHashString = Data(publicKeyHash).base64EncodedString() // เปรียบเทียบกับแฮชที่คาดหวัง return publicKeyHashString == expectedPublicKeyHash } }

สิ่งที่โค้ดนี้ทำ ทีละขั้นตอน:

  1. urlSession(_:didReceive:completionHandler:) - เมธอดนี้ถูกเรียกเมื่อแอปของคุณสร้างการเชื่อมต่อ HTTPS iOS ถามว่า: "ควรไว้วางใจใบรับรองของเซิร์ฟเวอร์นี้หรือไม่?"

  2. ตรวจสอบวิธีการรับรองความถูกต้อง - เราตรวจสอบว่านี้เป็นการท้าทายความไว้วางใจของเซิร์ฟเวอร์ (การตรวจสอบใบรับรอง) ไม่ใช่ประเภทการรับรองความถูกต้องอื่น

  3. ตรวจสอบความถูกต้องของใบรับรอง - เราเรียก isValidServerTrust() ซึ่ง:

    • ดึงใบรับรองของเซิร์ฟเวอร์จากการเชื่อมต่อ

    • รับคีย์สาธารณะจากใบรับรองนั้น

    • แฮชคีย์สาธารณะโดยใช้ SHA-256

    • เปรียบเทียบแฮชกับแฮชที่คาดหวังที่จัดเก็บไว้

  1. ตัดสินใจ:
  • ถ้าแฮชตรงกัน เซิร์ฟเวอร์จึงถูกต้อง ดำเนินการต่อด้วย .useCredential

  • ถ้าแฮชไม่ตรง เรามีการโจมตี MITM ที่เป็นไปได้ ยกเลิกด้วย .cancelAuthenticationChallenge

ดังนั้นวิธีนี้ป้องกันการโจมตี MITM ได้อย่างไร? แม้ผู้โจมตีจะติดตั้งใบรับรองปลอมบนอุปกรณ์ของผู้ใช้และดักจับการสื่อสาร แฮชของใบรับรองของพวกเขาจะไม่ตรงกับแฮшที่คุณผูกปิดไว้ แอปของคุณจะปฏิเสธการเชื่อมต่อ ป้องกันไม่ให้ผู้โจมตีถอดรหัสการสื่อสารของคุณได้

3. การป้องกันเพิ่มเติม: แนะนำ VPN บน Wi-Fi สาธารณะ

คุณยังสามารถแนะนำให้ผู้ใช้เชื่อมต่อผ่าน VPN เมื่อใช้ Wi-Fi สาธารณะเพื่อชั้นความปลอดภัยเพิ่มเติม และให้คำแนะนำเกี่ยวกับวิธีใช้ VPN อย่างมีประสิทธิภาพเพื่อรักษาความปลอดภัยของข้อมูล

สำหรับนักพัฒนา การรักษาความปลอดภัยของแอปที่แข็งแกร่งยังขึ้นอยู่กับการมีระบบที่เพิ่มประสิทธิภาพดี การเรียนรู้วิธีทำให้ Mac ช้าทำงานเร็วขึ้นสามารถช่วยให้มั่นใจได้ว่าการสร้าง (build) เรียบร้อย การทดสอบเร็วขึ้น และเวิร์กโฟลว์การพัฒนาที่ปลอดภัยมากขึ้นโดยรวม

3. การตรวจสอบความถูกต้องของอินพุตที่ไม่เหมาะสม

นักพัฒนาบางส่วนไม่สนใจการตรวจสอบความถูกต้องของอินพุตอย่างถูกต้อง นำไปสู่ช่องโหว่หลายประการ รวมถึง SQL injection การรันโค้ดระยะไกล และการเสียหายของข้อมูล

ในขณะที่ Swift มีการรองรับการกำหนดชนิดข้อมูล (strong typing) ที่แข็งแกร่ง นักพัฒนาบางส่วนไม่ทำความสะอาดอินพุตของผู้ใช้หรือการตอบสนองจาก API การรวมการตรวจสอบอีเมล API แบบเรียลไทม์ช่วยให้แน่ใจว่าผู้ใช้ให้ที่อยู่อีเมลที่ถูกต้องและมีรูปแบบเหมาะสมก่อนที่จะจัดเก็บหรือประมวลผล ลดทั้งความเสี่ยงด้านความปลอดภัยและปัญหาคุณภาพข้อมูล

วิธีแก้ไข:

การตรวจสอบความถูกต้องของอินพุตเป็นเส้นป้องกันแรกของคุณต่อข้อมูลที่เป็นอันตราย นี่คือวิธีปกป้องแอปพลิเคชัน iOS ของคุณ:

1. ตรวจสอบความถูกต้องของอินพุตผู้ใช้ด้วยรูปแบบ (Patterns)

ตรวจสอบความถูกต้องของอินพุตผู้ใช้โดยใช้ regular expressions หรือรูปแบบที่กำหนดไว้ล่วงหน้าก่อนประมวลผล ตัวอย่างเช่น เมื่อรับที่อยู่อีเมล:

func isValidEmail(_ email: String) -> Bool { let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex) return emailPredicate.evaluate(with: email) } // รับเฉพาะข้อมูลที่มีรูปแบบถูกต้อง guard isValidEmail(userEmail) else { // ปฏิเสธอินพุตที่ไม่ถูกต้อง return }

สิ่งนี้ทำให้แน่ใจว่าเฉพาะข้อมูลที่มีรูปแบบถูกต้องเท่านั้นที่ถูกยอมรับ ป้องกันไม่ให้อินพุตที่ผิดรูปแบบหรือเป็นอันตรายเข้าไปในระบบของคุณ

2. ทำความสะอาดการตอบสนองจาก API

อย่าเชื่อมั่นในข้อมูลภายนอก เสมอ ตรวจสอบความถูกต้องและทำความสะอาดการตอบสนองจาก API ก่อนนำไปใช้:

if let userAge = apiResponse["age"] as? Int, userAge >= 0 && userAge <= 150 { // ปลอดภัยที่จะใช้ user.age = userAge } else { // จัดการข้อมูลที่ไม่ถูกต้องอย่างเหมาะสม throw ValidationError.invalidAge }

3. ใช้ Parameterized Queries (สำคัญที่สุด)

ข้อผิดพลาดที่อันตรายที่สุดคือการสร้างคำสั่งฐานข้อมูลผ่านการต่อสตริง พิจารณาโค้ดที่เปราะบางนี้:

// ❌ อย่าทำเช่นนี้เลย - เปราะบางต่อ SQL Injection let username = userInput // อาจเป็น: "admin' OR '1'='1" let query = "SELECT * FROM users WHERE username = '\(username)'" database.execute(query)

หากผู้ใช้ที่เป็นอันตรายป้อน admin' OR '1'='1 เป็นชื่อผู้ใช้ คำสั่งจะกลายเป็น:

SELECT * FROM users WHERE username = 'admin' OR '1'='1'

สิ่งนี้จะคืนค่าผู้ใช้ทั้งหมดในฐานข้อมูลแทนที่จะเป็นเพียงคนเดียว อาจเปิดเผยข้อมูลที่ละเอียดอ่อน วิธีแก้ที่ปลอดภัยคือใช้ parameterized queries:

// ✅ ปลอดภัย - ใช้ parameterized queries let query = "SELECT * FROM users WHERE username = ?" database.execute(query, withArgumentsIn: [username])

ในวิธีนี้ ? เป็นตัวแทนที่ฐานข้อมูลถือว่าเป็นพารามิเตอร์ ไม่ใช่ส่วนหนึ่งของคำสั่ง SQL ค่า username ถูกส่งแยกต่างหากในอาร์เรย์ withArgumentsIn

นี่หมายความว่าแม้ผู้ใช้จะพยายามฉีดโค้ด SQL เช่น admin' OR '1'='1 ฐานข้อมูลจะถือสตริงทั้งหมดเป็นชื่อผู้ใช้ตามตัวอักษรที่ต้องค้นหา - ไม่ใช่โค้ด SQL ที่สามารถรันได้ เครื่องยนต์ฐานข้อมูลจะ escape ตัวอักษรพิเศษโดยอัตโนมัติ กำจัดความเสี่ยง SQL injection ทั้งหมด

โดยการแยกโครงสร้างคำสั่งออกจากข้อมูล parameterized queries 確保ว่าอินพุตของผู้ใช้ไม่สามารถเปลี่ยนแปลงตรรกะที่ตั้งใจไว้ของคำสั่ง SQL ของคุณได้

4. การฮาร์ดโค้ดข้อมูลลับ (Hardcoding Secrets)

API keys ข้อมูลรับรอง หรือโทเคนส่วนตัวที่ฮาร์ดโค้ดในโค้ดต้นทางเป็นข้อผิดพลาดด้านความปลอดภัยที่ร้ายแรง ผู้โจมตีสามารถดึงข้อมูลลับเหล่านี้จากไบนารีที่คอมไพล์แล้วโดยใช้เครื่องมือวิศวกรรมย้อนกลับ โดยเฉพาะแอปที่เผยแพร่สู่สาธารณะ

เมื่อถูกเปิดเผย ข้อมูลรับรองเหล่านี้สามารถใช้เข้าถึงบริการแบ็กเอนด์ของคุณได้ อาจนำไปสู่การรั่วไหลของข้อมูลหรือค่าใช้จ่ายที่ไม่ได้รับอนุญาต

ปัญหา - ข้อมูลลับที่ฮาร์ดโค้ด:

// อย่าทำเช่นนี้เลย class APIClient { private let apiKey = "1234567890abcdef" private let secretToken = "sk_live_51H..." func makeRequest() { // ข้อมูลลับเหล่านี้ฝังอยู่ในไบนารีของคุณ let headers = ["Authorization": "Bearer \(apiKey)"] } }

วิธีแก้ไข:

อย่าเก็บข้อมูลรับรองที่ละเอียดอ่อนไว้ในโค้ดโดยตรง นี่คือทางเลือกที่ปลอดภัย:

วิธีแก้ 1: ดึงข้อมูลลับจากแบ็กเอนด์ใน Runtime

วิธีที่ปลอดภัยที่สุดคือไม่เก็บข้อมูลลับบนไคลเอนต์เลย แทนที่จะเป็นเช่นนั้น ให้รับรองความถูกต้องของผู้ใช้และให้แบ็กเอนด์ของคุณทำการเรียก API ที่ได้รับอนุญาต:

class APIClient { private var sessionToken: String? // ผู้ใช้ล็อกอินและได้รับโทเคนเซสชันชั่วคราว func authenticateUser(email: String, password: String) async throws { let response = try await backend.login(email: email, password: password) // จัดเก็บเฉพาะโทเคนเซสชันชั่วคราวที่เฉพาะเจาะจงต่อผู้ใช้ self.sessionToken = response.sessionToken } // แบ็กเอนด์จัดการการเรียก API จริงด้วย API key จริง func fetchUserData() async throws -> UserData { guard let token = sessionToken else { throw AuthError.notAuthenticated } // แบ็กเอนด์ของคุณรับคำขอนี้ ตรวจสอบโทเคนเซสชัน // จากนั้นใช้ API key ของตัวเองที่จัดเก็บอย่างปลอดภัยเพื่อดึงข้อมูลจากบริการบุคคลที่สาม return try await backend.getUserData(sessionToken: token) } }

วิธีการทำงาน:

แอปของคุณไม่เคยรู้ API key จริง เมื่อผู้ใช้ต้องการข้อมูล แอปจะส่งคำขอไปยังเซิร์ฟเวอร์แบ็กเอนด์ของตัวเองพร้อมกับโทเคนเซสชัน แบ็กเอนด์ตรวจสอบโทเคน จากนั้นใช้ API key ของตัวเองที่จัดเก็บอย่างปลอดภัยเพื่อเรียก API ของบุคคลที่สามจริง ด้วยวิธีนี้ ข้อมูลลับจริงจะไม่เคยออกจากเซิร์ฟเวอร์ของคุณ

วิธีแก้ 2: ตัวแปรสภาพแวดลมหรือไฟล์ Config (สำหรับการพัฒนาเท่านั้น)

สำหรับสภาพแวดล้อมการพัฒนา ใช้ไฟล์ .xcconfig ที่ถูกยกเว้นจากการควบคุมเวอร์ชัน:

// Secrets.xcconfig (เพิ่มใน .gitignore!) API_KEY = your_dev_api_key_here API_SECRET = your_dev_secret_here // เข้าถึงในโค้ดผ่าน Info.plist class Config { static let apiKey: String = { guard let key = Bundle.main.object(forInfoDictionaryKey: "API_KEY") as? String else { fatalError("API_KEY not found in configuration") } return key }() }

สำคัญ: วิธีนี้เหมาะสมเฉพาะสภาพแวดล้อมที่ไม่ใช่ production เท่านั้น! อย่าส่ง production API keys ไปกับแอปของคุณ แม้ในไฟล์ config ก็ตาม

5. การตรวจสอบสิทธิ์และการอนุญาตที่ไม่เพียงพอ

การพึ่งพาการตรวจสอบสิทธิ์และการอนุญาตฝั่งไคลเอนต์เป็นเรื่องเสี่ยง ผู้โจมตีสามารถทำให้แอปข้ามการตรวจสอบเหล่านี้และเข้าถึงโดยไม่ได้รับอนุญาตได้โดยการ brute force หรือการแก้ไขแอป/รันไทม์

วิธีแก้ไข:

  • ทำการตรวจสอบสิทธิ์และการอนุญาตฝั่งเซิร์ฟเวอร์แทนที่จะเป็นฝั่งไคลเอนต์

  • ใช้ JWT (JSON Web Tokens) หรือ OAuth 2.0 สำหรับการล็อกอินผู้ใช้ที่ได้รับการรับรอง

  • ต้องมีการหมดอายุโทเคนและตรรกะการรีเฟรช เพื่อลดความน่าจะเป็นของการขโมยโทเคน

ตัวอย่าง: การส่ง JWT อย่างปลอดภัย:

let request = URLRequest(url: apiURL) request.setValue("Bearer \(jwtToken)", forHTTPHeaderField: "Authorization")

6. การบันทึกบันทึก (Logging) และการจัดการข้อผิดพลาดที่ไม่ปลอดภัย

การบันทึกบันทึกที่กว้างขวางและไม่ปลอดภัย รวมถึงข้อยกเว้นที่ไม่ได้จับ (uncaught exceptions) อาจนำไปสู่การเปิดเผยข้อมูลที่ละเอียดอ่อน รวมถึงชื่อผู้ใช้ รหัสผ่าน และ API keys

วิธีแก้ไข:

  • บันทึกข้อมูลที่ละเอียดอ่อนอย่างระมัดระวัง

  • ใช้การจัดการข้อผิดพลาดที่ควบคุมได้ และให้ข้อมูลน้อยที่สุดในข้อความที่แสดงให้ผู้ใช้เห็น

  • ใช้ไลบรารีการบันทึกบันทึกที่ปลอดภัยซึ่งปิดบังหรือเข้ารหัสข้อมูลส่วนตัว

do { try someRiskyOperation() } catch { // บันทึกข้อผิดพลาดอย่างปลอดภัย Logger.log("Operation failed: \(error.localizedDescription)") }

7. การละเลยการปิดบังโค้ดและการวิศวกรรมย้อนกลับ

ไบนารี Swift สามารถถูกวิศวกรรมย้อนกลับเพื่อเปิดเผยตรรกะทางธุรกิจที่ละเอียดอ่อน อัลกอริทึม หรือข้อมูลลับที่ซ่อนอยู่ ผู้โจมตีใช้เครื่องมืออย่าง Hopper Disassembler, class-dump หรือ IDA Pro เพื่อถอดคอมไพล์แอปของคุณและวิเคราะห์การทำงานภายใน ความเสี่ยงนี้มักถูกประเมินต่ำเกินไป โดยเฉพาะสำหรับแอปขนาดเล็ก แต่อย่างใดแอปใดก็สามารถเป็นเป้าหมายได้

นี่หมายความว่าเมื่อคุณคอมไพล์แอป Swift ไบนารีที่ได้จะมี:

  • ชื่อคลาสและลายเซ็นเมธอด

  • สตริงลิเทอรัล (URLs, ข้อความข้อผิดพลาด, คีย์)

  • โครงสร้างตรรกะโค้ดของคุณ

  • การใช้งานอัลกอริทึม

ผู้โจมตีสามารถดึงข้อมูลนี้และใช้เพื่อเข้าใจขั้นตอนการรับรองความถูกต้องของแอปและข้ามมันได้ คัดลอกอัลกอริทึมที่เป็นความเป็นเอกสิทธิ์ ค้นหา API endpoints หรือคีย์ที่ฮาร์ดโค้ดซึ่งคุณคิดว่า "ซ่อนอยู่" ค้นพบฟีเจอร์พรีเมียมที่จะปลดล็อกโดยไม่ต้องจ่ายเงิน และอื่นๆ

ทำไมจึงไม่ดี - ตัวอย่างจริง:

สมมติว่าคุณมีการตรวจสอบฟีเจอร์พรีเมียมในแอป:

class FeatureManager { func isPremiumUser() -> Bool { // ตรวจสอบว่าผู้ใช้มีการเข้าถึงพรีเมียมหรือไม่ let hasSubscription = UserDefaults.standard.bool(forKey: "premium_unlocked") return hasSubscription } func unlockPremiumFeature() { guard isPremiumUser() else { showPaywall() return } // แสดงเนื้อหาพรีเมียม showPremiumContent() } }

ผู้โจมตีสามารถวิศวกรรมย้อนกลับแอปของคุณและค้นพบว่าเมธอด isPremiumUser() ควบคุมการเข้าถึง และมันเพียงแค่ตรวจสอบคีย์ UserDefaults ที่ชื่อ premium_unlocked พวกเขาจะรู้ว่าสามารถใช้เครื่องมือจัดการรันไทม์ตั้งค่านี้เป็น true ได้ ข้ามเพย์วอลล์ของคุณได้ทั้งหมด

วิธีแก้ไข:

1. ใช้การเพิ่มประสิทธิภาพของคอมไพเลอร์ Swift

เปิดใช้งานฟลักการเพิ่มประสิทธิภาพที่ลบสัญลักษณ์การดีบักและทำให้ไบนารีอ่านยากขึ้น:

// ใน Build Settings: // - ตั้ง "Optimization Level" เป็น "-O" (หรือ -Osize) สำหรับ release builds // - เปิด "Strip Debug Symbols During Copy" = YES // - ตั้ง "Strip Style" เป็น "All Symbols"

สิ่งนี้ลบชื่อฟังก์ชันและทำให้โค้ดที่คอมไพล์แล้วอ่านยากขึ้น - แม้ชื่อคลาส/เมธอดจะยังมองเห็นได้บางส่วน

2. ใช้เครื่องมือปิดบังสัญลักษณ์ (Symbol Obfuscation Tools)

เครื่องมืออย่าง SwiftShield สามารถเปลี่ยนชื่อคลาส เมธอด และพร็อพเพอร์ตี้ของคุณเป็นชื่อที่ไร้ความหมาย:

// ก่อนปิดบัง (อ่านง่ายสำหรับผู้โจมตี): class FeatureManager { func isPremiumUser() -> Bool { ... } } // หลังปิดบัง (เข้าใจยากขึ้น): class a7f3b2 { func x9k2m() -> Bool { ... } }

แม้สิ่งนี้จะไม่ป้องกันการวิศวกรรมย้อนกลับ แต่ก็ทำให้ผู้โจมตียากขึ้นอย่างมากในการเข้าใจว่าโค้ดทำอะไร

3. ย้ายตรรกะที่ละเอียดอ่อนไปยังเซิร์ฟเวอร์ (Best Practice)

แทนที่จะตรวจสอบสถานะพรีเมียมในเครื่อง ให้ตรวจสอบฝั่งเซิร์ฟเวอร์:

// ✅ วิธีที่ปลอดภัย - เซิร์ฟเวอร์ตรวจสอบทุกอย่าง class FeatureManager { func unlockPremiumFeature() async { do { // เซิร์ฟเวอร์ตรวจสอบว่าผู้ใช้มีสิทธิ์เข้าถึงพรีเมียมจริงๆ let hasAccess = try await backend.verifyPremiumAccess(userId: currentUserId) if hasAccess { showPremiumContent() } else { showPaywall() } } catch { // จัดการข้อผิดพลาด showPaywall() } } }

วิธีการทำงาน:

แบ็กเอนด์ของคุณเป็นแหล่งข้อมูลที่เชื่อถือได้ (source of truth) เกี่ยวกับการเข้าถึงพรีเมียม แม้ผู้โจมตีจะวิศวกรรมย้อนกลับแอปและพยายามข้ามการตรวจสอบ เซิร์ฟเวอร์จะปฏิเสธคำขอที่ไม่ได้รับอนุญาต แอปกลายเป็นเพียงเลเยอร์ UI ในขณะที่การตัดสินใจสำคัญทั้งหมดเกิดขึ้นฝั่งเซิร์ฟเวอร์ที่ปลอดภัย - ที่ผู้โจมตีไม่สามารถจัดการได้

หลักการสำคัญคือให้สมมติว่าโค้ดแอปของคุณเป็นสาธารณะ: อย่าพึ่งพาการตรวจสอบฝั่งไคลเอนต์สำหรับการดำเนินการที่สำคัญต่อความปลอดภัย เช่น การชำระเงิน การควบคุมการเข้าถึง หรือการรับรองความถูกต้อง ใช้การปิดบังเพื่อทำให้การวิศวกรรมย้อนกลับยากขึ้น แต่ในที่สุดให้ย้ายตรรกะที่ละเอียดอ่อนไปยังแบ็กเอนด์ที่ปลอดภัยของคุณ

8. ไลบรารีของบุคคลที่สามที่ไม่ปลอดภัย

ไลบรารีของบุคคลที่สามมีความเสี่ยงหากถูกเจาะหรือล้าสมัย นักพัฒนาอาจให้ความสำคัญกับฟังก์ชันของแอปมากกว่าความเสี่ยงด้านความปลอดภัยจาก dependencies โดยไม่ได้ตั้งใจ และเครื่องมือ ETL สามารถช่วยเพิ่มเติมได้โดยการเพิ่มประสิทธิภาพการตรวจสอบและประมวลผลข้อมูลที่เกี่ยวข้องกับ dependencies เพื่อระบุช่องโหว่อย่างมีประสิทธิภาพมากขึ้น

ในมุมมองที่กว้างขึ้น การนำแนวทางปฏิบัติด้านความปลอดภัยศูนย์ข้อมูลที่แข็งแกร่งไปใช้ จะช่วยให้แน่ใจว่าแม้ส่วนประกอบของบุคคลที่สามจะนำความเสี่ยงมา สโครงสร้างพื้นฐานที่รองรับก็ยังคงมีความทนทานต่อการโจมตี

วิธีแก้ไข:

  • ใช้ไลบรารีที่มีคุณภาพสูงและได้รับการดูแลรักษาอย่างดีเท่านั้น

  • อัปเดต dependencies และติดตาม CVEs (Common Vulnerabilities and Exposures)

  • ตรวจสอบโค้ดไลบรารีหากจัดการข้อมูลที่ละเอียดอ่อน

9. การตรวจสอบสิทธิ์ชีวภาพและหลายปัจจัยที่ไม่เพียงพอ

แอปพลิเคชันส่วนใหญ่พึ่งพารหัสผ่านเพียงอย่างเดียว ซึ่งเปราะบางต่อการถูกแฮก การเปิดใช้งานชีวภาพอย่าง Face ID หรือ Touch ID เพิ่มความปลอดภัยให้กับผู้ใช้

วิธีแก้ไข:

  • ใช้ LocalAuthentication framework สำหรับการรับรองความถูกต้องด้วยชีวภาพ

  • รวมชีวภาพกับการรับรองความถูกต้องฝั่งเซิร์ฟเวอร์สำหรับการรับรองหลายปัจจัย (MFA)

import LocalAuthentication let context = LAContext() var error: NSError? if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) { context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "Access your account") { success, authError in DispatchQueue.main.async { if success { // ดำเนินการอย่างปลอดภัย } else { // จัดการความล้มเหลว (authError อาจมีเหตุผล) print("Authentication failed: \(authError?.localizedDescription ?? "Unknown error")") } } } } else { // ชีวภาพไม่พร้อมใช้งาน ตรวจสอบ error เพื่อรายละเอียด print("Biometrics unavailable: \(error?.localizedDescription ?? "Unknown error")") }

10. การไม่สนใจการทดสอบความปลอดภัยเป็นระยะ

แอปแม้จะปฏิบัติตามแนวทางปฏิบัติที่ดีที่สุดในช่วงการพัฒนา ก็มักมีช่องโหว่ที่ยังไม่ถูกค้นพบซึ่งเกิดจากการโต้ตอบที่ซับซ้อน dependencies ของบุคคลที่สาม หรือเวกเตอร์การโจมตีที่ค้นพบใหม่ การทดสอบความปลอดภัยเป็นระยะจึงเป็นสิ่งจำเป็นอย่างยิ่งเพื่อค้นพบช่องโหว่เหล่านี้ก่อนที่ผู้โจมตีจะใช้ประโยชน์

การทดสอบความปลอดภัยควรเกิดขึ้นในหลายขั้นตอน โดยใช้เครื่องมือและแนวทางที่เข้าถึงได้:

  1. การสแกนความปลอดภัยอัตโนมัติ: รันทุกครั้งที่ build เพื่อจับปัญหาทั่วไป

  2. การตรวจสอบโค้ดด้วยตนเอง: การทบทวนโค้ดที่เน้นความปลอดภัยเป็นประจำโดยใช้แนวทางที่กำหนดไว้

  3. เครื่องมือสแกนช่องโหว่: ใช้เครื่องมือฟรีอย่าง MobSF เพื่อวิเคราะห์แอปเพื่อหาช่องโหว่ด้านความปลอดภัย

  4. การตรวจสอบ Dependencies: ตรวจสอบไลบรารีบุคคลที่สามสำหรับช่องโหว่ด้านความปลอดภัยที่ทราบ

วิธีแก้ไข:

1. ใช้งานการสแกนความปลอดภัยอัตโนมัติใน CI/CD

รวมเครื่องมือสแกนความปลอดภัยเข้าไปในไปป์ไลน์ continuous integration ของคุณ เพื่อให้ทุกการเปลี่ยนแปลงโค้ดถูกตรวจสอบโดยอัตโนมัติ:

# ตัวอย่าง: GitHub Actions workflow สำหรับการสแกนความปลอดภัยอัตโนมัติ name: Security Scan on: [push, pull_request] jobs: security-scan: runs-on: macos-latest steps: - name: Checkout code uses: actions/checkout@v3 - name: Run MobSF Security Scan run: | # Mobile Security Framework - สแกนหาช่องโหว่ทั่วไป docker run -v $(pwd):/app opensecurity/mobile-security-framework-mobsf - name: Dependency Vulnerability Check run: | # ตรวจสอบ dependencies CocoaPods/SPM สำหรับ CVEs ที่ทราบ brew install dependency-check dependency-check --scan ./Podfile.lock --format JSON - name: Secret Detection run: | # ตรวจจับข้อมูลลับที่ commit ไปโดยไม่ได้ตั้งใจ brew install truffleHog truffleHog filesystem . --json - name: Fail build on critical issues run: | if grep -q "CRITICAL" security-report.json; then echo "Critical security issues found!" exit 1 fi

การสแกนอัตโนมัติตรวจสอบ:

  • API keys, โทเคน หรือรหัสผ่านที่ฮาร์ดโค้ด

  • การตั้งค่าเครือข่ายที่ไม่ปลอดภัย (อนุญาตให้ส่ง HTTP แทน HTTPS)

  • อัลกอริทึมเข้ารหัสที่อ่อนแอ

  • ช่องโหว่ที่ทราบในไลบรารีบุคคลที่สาม

  • การตรวจสอบใบรับรอง SSL/TLS ที่ไม่เหมาะสม

  • การจัดเก็บข้อมูลที่ไม่ปลอดภัย (จัดเก็บข้อมูลละเอียดอ่อนใน UserDefaults)

  • สิทธิ์แอปที่มากเกินไป

ตัวอย่างผลลัพธ์จากการสแกนอัตโนมัติ:

Security Scan Results: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ [CRITICAL] Hardcoded API Key Found File: APIClient.swift:15 Issue: API key "sk_live_abc123..." detected in source code [HIGH] Insecure HTTP Connection File: NetworkManager.swift:42 Issue: App allows cleartext HTTP traffic to api.example.com Fix: Enforce HTTPS or add exception to Info.plist if required [MEDIUM] Weak Encryption Algorithm File: DataEncryption.swift:28 Issue: Using MD5 for hashing (cryptographically broken) Fix: Use SHA-256 or better [LOW] Outdated Dependency Library: Alamofire 4.2.0 Issue: Known vulnerability CVE-2021-12345 Fix: Update to version 5.6.0 or later ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Build Failed: 2 critical issues must be fixed before deployment

2. ใช้ MobSF (Mobile Security Framework) สำหรับการวิเคราะห์ช่องโหว่

MobSF เป็นเครื่องมือฟรีแบบอัตโนมัติที่วิเคราะห์แอป iOS ของคุณเพื่อหาปัญหาด้านความปลอดภัย:

# ติดตั้งและรัน MobSF ในเครื่อง docker pull opensecurity/mobile-security-framework-mobsf docker run -it -p 8000:8000 opensecurity/mobile-security-framework-mobsf # อัปโหลดไฟล์ .ipa ผ่านเว็บอินเทอร์เฟซที่ localhost:8000 # MobSF จะวิเคราะห์และให้รายงานความปลอดภัยโดยละเอียด

สิ่งที่ MobSF ตรวจสอบ:

  • การวิเคราะห์ไบนารีสำหรับข้อมูลลับที่ฮาร์ดโค้ด

  • รูปแบบการจัดเก็บข้อมูลที่ไม่ปลอดภัย

  • การใช้งานเข้ารหัสที่อ่อนแอ

  • การเชื่อมต่อเครือข่ายที่ไม่ปลอดภัย

  • คุณภาพโค้ดและแนวทางปฏิบัติด้านความปลอดภัยที่ดีที่สุด

  • การปฏิบัติตามมาตรฐานความปลอดภัย

3. ทำการตรวจสอบโค้ดเป็นระยะโดยใช้ OWASP MSTG

ใช้ OWASP Mobile Security Testing Guide เป็นรายการตรวจสอบ (checklist) เพื่อตรวจสอบโค้ดของตัวเอง:

// ตัวอย่าง: ปฏิบัติตามคำแนะนำ OWASP MSTG สำหรับการจัดเก็บที่ปลอดภัย class SecureStorage { // ❌ ไม่ปลอดภัย - UserDefaults ไม่ได้เข้ารหัส func saveTokenInsecurely(_ token: String) { UserDefaults.standard.set(token, forKey: "authToken") } // ✅ ปลอดภัย - ใช้ Keychain ตามที่ OWASP แนะนำ func saveTokenSecurely(_ token: String) throws { let data = token.data(using: .utf8)! let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: "authToken", kSecValueData as String: data, kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly ] SecItemDelete(query as CFDictionary) let status = SecItemAdd(query as CFDictionary, nil) guard status == errSecSuccess else { throw StorageError.saveFailed } } }

OWASP MSTG Checklist สำหรับการตรวจสอบด้วยตนเอง:

  • [ ] ข้อมูลที่ละเอียดอ่อนทั้งหมดถูกเข้ารหัสขณะจัดเก็บหรือไม่?

  • [ ] HTTPS ถูกบังคับใช้สำหรับการเรียกเครือข่ายทั้งหมดหรือไม่?

  • [ ] ใบรับรองได้รับการตรวจสอบอย่างเหมาะสมหรือไม่?

  • [ ] ข้อมูลที่ละเอียดอ่อนถูกตัดออกจาก logs หรือไม่?

  • [ ] API keys และข้อมูลลับไม่ได้ถูกฮาร์ดโค้ดหรือไม่?

  • [ ] อินพุตของผู้ใช้ได้รับการตรวจสอบและทำความสะอาดหรือไม่?

  • [ ] โทเคนการรับรองความถูกต้องถูกจัดเก็บอย่างปลอดภัยใน Keychain หรือไม่?

4. การสแกน Dependencies อัตโนมัติ

ติดตาม dependencies ของคุณอย่างต่อเนื่องสำหรับช่องโหว่ที่ค้นพบใหม่:

# สำหรับโปรเจกต์ CocoaPods gem install cocoapods-audit pod audit # สำหรับ Swift Package Manager # ใช้ GitHub Dependabot (ฟรีสำหรับ public repos) หรือ brew install swift-outdated swift-outdated

และตั้งค่าการแจ้งเตือนอัตโนมัติด้วยเครื่องมือเหล่านี้:

  • GitHub Dependabot: สร้าง PR อัตโนมัติเมื่อตรวจพบ dependencies ที่มีช่องโหว่ (ฟรี)

  • Snyk: มีระดับฟรีสำหรับโปรเจกต์โอเพนซอร์ส

  • OWASP Dependency-Check: เครื่องมือบรรทัดคำสั่งฟรี

สรุป

การพัฒนาแอป iOS ที่ปลอดภัยด้วย Swift 全在于การคิดล่วงหน้า คุณควรทำทุกอย่างเพื่อหลีกเลี่ยงข้อผิดพลาดทั่วไปเหล่านี้ เช่น การจัดเก็บข้อมูลที่ไม่ปลอดภัย การสื่อสารเครือข่ายที่ล้มเหลว ข้อมูลลับที่ฮาร์ดโค้ด หรือการรับรองความถูกต้องที่อ่อนแอ

การใช้ Keychain สำหรับข้อมูลลับ การบังคับ HTTPS การตรวจสอบความถูกต้องของอินพุต และการรับรองหลายปัจจัย ล้วนเป็นขั้นตอนที่ลดความเสี่ยง

การทดสอบช่องโหว่ด้านความปลอดภัยเป็นระยะและการจำกัดการใช้ไลบรารีบุคคลที่สาม สามารถเสริมสร้างความปลอดภัยของแอปได้อีกด้วย

ความปลอดภัยเป็นหน้าที่ที่ต่อเนื่อง Swift มีเครื่องมือ แต่เป็นนักพัฒนาที่ต้องนำเครื่องมือเหล่านั้นไปใช้อย่างระมัดระวัง การจัดการความปลอดภัยตั้งแต่เริ่มต้นจะปกป้องข้อมูลผู้ใช้ สร้างความไว้วางใจ และปกป้องชื่อเสียงของแอปพลิเคชัน

เรียนรู้การเขียนโค้ดฟรี หลักสูตรโอเพนซอร์สของ freeCodeCamp ช่วยให้มากกว่า 40,000 คนได้งานเป็นนักพัฒนา เริ่มต้นเลยวันนี้