Java原始碼安全審查
最近業務需要出一份Java Web應用原始碼安全審查報告, 對比了市面上數種工具及其分析結果, 基於結果總結了一份規則庫.本文目錄結構如下:
檢測工具
FindSecurityBugs
基於class檔案分析, 他是大名鼎鼎的findbugs的外掛, 安裝比較簡單.在findbugs官網下載安裝包,外掛jar,把jar放到findbugs-3.0.1\plugin目錄.
開啟bin路徑下的findbugs.bat啟動軟體.在選單欄 - 編輯 - 選項可以檢視外掛是否啟用成功.
新建專案, 輸入名稱, 選擇需要分析的class路徑, 引用的第三方包地址, 原始碼路徑, 點選Analyze即可.
最終生成的結果可以轉為html報告, 也可以匯出xml檔案, 在findbugs分析檢視. 本文主要關注Security一欄.
程式碼衛士
360出品, 名聲似乎不太好, 誤報比較多, 不過結果也有一定的參考價值.如果程式碼在碼雲的話, 點服務一欄, 可以線上掃描碼雲庫裡的原始碼, 原生代碼要去官網申請試用.
掃描結果
Fortify
HP出品的老牌掃描工具, 網上有破解的. 安裝過程一路next即可.最後啟動AuditWorkBench, 選擇scan java project, 耐心等待.
結果跟findbugs類似.
CodePecker
啄木鳥原始碼分析, 國內的一款也是基於位元組碼分析的工具, 提供了一個收費的
結果分析
程式碼注入
命令注入
命令注入是指應用程式執行命令的字串或字串的一部分來源於不可信賴的資料來源,程式沒有對這些不可信賴的資料進行驗證、過濾,導致程式執行惡意命令的一種攻擊方式。
String dir = request.getParameter("dir"); Process proc = Runtime.getRuntime().exec("cmd.exe /c dir" + dir);
如果攻擊者傳遞了一個dir形式為"dummy &&delc:\\dbms\\*.*"的字串,那麼該段程式碼將會在執行其他指定命令的同時執行這條刪除命令。
修復方式
(1)程式對非受信的使用者輸入資料進行淨化,刪除不安全的字元。
(2)限定輸入型別, 建立一份安全字串列表,限制使用者只能輸入該列表中的資料。
1 // 方式1 2 if (!Pattern.matches("[0-9A-Za-z@.]+", dir)) { 3 // Handle error 4 } 5 6 // 方式2 7 int number = Integer.parseInt(request.getParameter("dir")); 8 switch (number) { 9 case 1: 10 btype = "tables" 11 break; // Option 1 12 case 2: 13 btype = "users" 14 break; // Option 2 15 ......
HTTP響應截斷
程式從一個不可信賴的資料來源獲取資料,未進行驗證就置於HTTP標頭檔案中發給使用者,可能會導致HTTP響應截斷攻擊。
String author = request.getParameter(AUTHOR_PARAM); ... Cookie cookie = new Cookie("author", author); cookie.setMaxAge(cookieExpiration); response.addCookie(cookie);
那麼如果攻擊者提交的是一個惡意字串,比如“WileyHacker\r\nHTTP/1.1200OK\r\n...”,那麼HTTP響應就會被分割成以下形式的兩個響應:
HTTP/1.1200OK
...
Set-Cookie:author=WileyHacker
HTTP/1.1200OK
...
這樣第二個響應已完全由攻擊者控制,攻擊者可以用所需的標頭檔案和正文內容構建該響應實施攻擊。
修復方式
防止HTTP響應截斷攻擊的最安全的方法是建立一份安全字元白名單,只接受完全由這些受認可的字元組成的輸入出現在HTTP響應標頭檔案中。
String author = request.getParameter(AUTHOR_PARAM); if (Pattern.matches("[0-9A-Za-z]+", author)) { ... Cookie cookie = new Cookie("author", author); cookie.setMaxAge(cookieExpiration); response.addCookie(cookie); }
SQL注入
SQL注入是一種資料庫攻擊手段。攻擊者通過嚮應用程式提交惡意程式碼來改變原SQL語句的含義,進而執行任意SQL命令,達到入侵資料庫乃至作業系統的目的。
String sqlString = "SELECT * FROM db_user WHERE username = '" + username + "' AND password = '" + pwd + "'"; Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery(sqlString);
攻擊者能夠替代username和password中的任意字串,它們可以使用下面的關於password的字串進行SQL注入。
SELECT*FROMdb_userWHEREusername=''ANDpassword=''OR'1'='1'
修復方式
造成SQL注入攻擊的根本原因在於攻擊者可以改變SQL查詢的上下文,使程式設計師原本要作為資料解析的數值,被篡改為命令了。防止SQL注入的方法如下:
(1)正確使用引數化API進行SQL查詢。
(2)如果構造SQL指令時需要動態加入約束條件,可以通過建立一份合法字串列表,使其對應於可能要加入到SQL指令中的不同元素,來避免SQL注入攻擊。
String sqlString = "select * from db_user where username=? and password=?"; PreparedStatement stmt = connection.prepareStatement(sqlString); stmt.setString(1, username); stmt.setString(2, pwd); ResultSet rs = stmt.executeQuery();
正則表示式注入
資料被傳遞至應用程式並作為正則表示式使用。可能導致執行緒過度使用CPU資源,從而導致拒絕服務攻擊。
下述程式碼java中字串的split, replaceAll均支援正則的方式, 導致CPU掛起.
1 final String input = "0000000000000000000000000000000000000000000000"; 2 long startTime = System.currentTimeMillis(); 3 System.out.println(input.split("(0*)*A")); 4 System.out.println("耗時:" + (System.currentTimeMillis() - startTime) + "ms");
-
該正則的意思是說匹配器在輸入的末尾並沒有檢測到”A”。現在外側的限定符後退一次,記憶體的則前進一次,如此重複,無法得到結果。
-
因此,匹配器逐步回退,並嘗試所有的組合以找出匹配符號。它最終將返回(沒有匹配的結果),但是該過程的複雜性是指數型的(輸入中新增一個字元加倍了執行時間)
修復方式
使用執行緒池 +Future, 限定執行時間, 並捕獲異常.
1 ExecutorService service = Executors.newFixedThreadPool(1); 2 Future result = service.submit(new Callable<Object>() { 3 @Override 4 public Object call() { 5 final String input = "0000000000000000000000000000000000000000000000"; 6 return input.split("(0*)*A"); 7 } 8 }); 9 service.shutdown(); 10 System.out.println(result.get(5, TimeUnit.SECONDS));
LDAP注入
LDAP注入是指客戶端傳送查詢請求時,輸入的字串中含有一些特殊字元,導致修改了LDAP本來的查詢結構,從而使得可以訪問更多的未授權資料的一種攻擊方式。
以下程式碼動態構造一個LDAP查詢,並對其加以執行,該查詢可以檢索所有報告給指定經理的僱員記錄。該經理的名字是從HTTP請求中讀取的,因此不可信任。
1 DirContext ctx = new InitialDirContext(env); 2 String managerName = request.getParameter("managerName"); 3 //retrieve all of the employees who report to a manager 4 String filter = "(manager=" + managerName + ")"; 5 NamingEnumeration employees = ctx.search("ou=People,dc=example,dc=com",filter);
如果攻擊者為managerName輸入字串Hacker,Wiley)(|(objectclass=*),則該查詢會變成:
(manager=Hacker,Wiley)(|(objectclass=*))
根據執行查詢的許可權,增加|(objectclass=*)條件會導致篩選器與目錄中的所有輸入都匹配,而且會使攻擊者檢索到有關使用者輸入池的資訊。
如果攻擊者能夠控制查詢的命令結構,那麼這樣的攻擊至少會影響執行LDAP查詢的使用者可以訪問的所有記錄。
修復方式
用白名單的方法,確保LDAP查詢中由使用者控制的數值完全來自於預定的字元集合,應不包含任何LDAP元字元。
比如使用Spring框架中EqualsFilter類來構造一個編碼得當的篩選器字串.
DirContext ctx = new InitialDirContext(env); String managerName = request.getParameter("managerName"); //retrieve all of the employees who report to a manager EqualsFilter filter = new EqualsFilter("manager", managerName); NamingEnumeration employees = ctx.search("ou=People,dc=example,dc=com",filter.toString());
輸入驗證
拒絕服務
拒絕服務是攻擊者通過極度消耗應用資源,以致程式崩潰或其他合法使用者無法進行使用的一種攻擊方式。
例如解壓檔案前,未檢查檔案大小,攻擊者可以通過提供一個超大檔案,實施DOS攻擊。
1 FileOutputStream fos = new FileOutputStream(entry.getName()); 2 dest = new BufferedOutputStream(fos, BUFFER); 3 while ((count = zis.read(data, 0, BUFFER)) != -1) { 4 dest.write(data, 0, count); 5 }
修復方式
對涉及到系統資源的外部資料應該進行嚴格校驗,防止無限制的輸入。對於使用者上傳的檔案, 要在後臺進行大小校驗.
比如對解壓檔案進行驗證,超過100M,將丟擲異常。
1 if (entry.getSize() > TOOBIG) { 2 throw new IllegalStateException("File to be unzipped is huge."); 3 }
重定向引數
應用程式允許未驗證的使用者輸入控制重定向中的URL,可能會導致攻擊者發動釣魚攻擊。
String url = request.getParameter("url"); response.sendRedirect(url);
修復方式
建立一份合法URL列表,使用者只能從中進行選擇,進行重定向操作。
XML實體注入
簡稱XXE攻擊, XML解析器中預設會解析xml中的ENTITY來支援全域性變數以及外部檔案讀取.
如果從web請求中獲取xml內容, 並在伺服器端解析, 則可能導致xxe攻擊.
1 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); 2 DocumentBuilder db = dbf.newDocumentBuilder(); 3 Document doc = db.parse(xmlFile); 4 NodeList list = doc.getElementsByTagName("active");
修復方式
(1)關閉XML實體解析
(2)使用JSON來替代XML做資料傳輸
1 DocumentBuilderFactory dbf =DocumentBuilderFactory.newInstance(); 2 dbf.setExpandEntityReferences(false);
資源注入
使用使用者輸入控制資源識別符號,藉此攻擊者可以訪問或修改其他受保護的系統資源。當滿足以下兩個條件時,就會發生資源注入:
(1)攻擊者可以指定已使用的識別符號來訪問系統資源。例如,攻擊者可能可以指定用來連線到網路資源的埠號。
(2)攻擊者可以通過指定特定資源來獲取某種許可權,而這種許可權在一般情況下是不可能獲得的。例如,程式可能會允許攻擊者把敏感資訊傳輸到第三方伺服器。
1 URL url = new URL(request.getParameter("remoteURL")); 2 URLConnection connection = url.openConnection(); 3 ... 4 String remotePort = request.getParameter("remotePort"); 5 ServerSocket srvr = new ServerSocket(remotePort); 6 Socket skt = srvr.accept();
修復方式
使用白名單策略, 限制資原始檔讀取和訪問.
日誌偽造
允許日誌記錄未經驗證的使用者輸入,會導致日誌偽造攻擊。攻擊者可能通過破壞檔案格式或注入意外的字元,從而使檔案無法使用。
更陰險的攻擊可能會導致日誌檔案中的統計資訊發生偏差,掩護攻擊者的跟蹤軌跡.
1 if (loginSuccessful) { 2 logger.severe("User login succeeded for: " + username); 3 } else { 4 logger.severe("User login failed for: " + username); 5 }
攻擊者可以將username替換為一個多行字串,如下所示:
jack
2013-7-30java.util.logging.LogManagerlog
Server:Userloginsucceededfor:Tom
修復方式
對不可信賴的資料進行校驗。
另外日誌中不應該出現敏感資料, 例如密碼, 手機號, 郵箱這些資訊.
1 if (!Pattern.matches("[A-Za-z0-9_]+", username)) { 2 // Unsanitized username 3 logger.severe("User login failed for unauthorized user"); 4 } else if (loginSuccessful) { 5 logger.severe("User login succeeded for: " + username); 6 }
檔案校驗
對於使用者上傳的檔案, 需要在前後臺雙重校驗, 校驗字尾, 檔案大小, 二進位制頭等等.
其他可輸入項, 也要做前後臺雙重校驗, 防止中間人修改資料.
修復方式
對檔案二進位制頭進行校驗
1 static { 2 MAGIC_NUMBER.put("jpg", new String[]{"FFD8"}); 3 MAGIC_NUMBER.put("gif", new String[]{"47494638"}); 4 MAGIC_NUMBER.put("png", new String[]{"89504E470D0A1A0A"}); 5 MAGIC_NUMBER.put("pdf", new String[]{"25504446"}); 6 MAGIC_NUMBER.put("doc", new String[]{"D0CF11E0A1B11AE1", "7B5C72746631"}); 7 MAGIC_NUMBER.put("xls", new String[]{"D0CF11E0A1B11AE1"}); 8 MAGIC_NUMBER.put("ppt", new String[]{"D0CF11E0A1B11AE1"}); 9 MAGIC_NUMBER.put("docx", new String[]{"504B0304"}); 10 MAGIC_NUMBER.put("xlsx", new String[]{"504B0304"}); 11 MAGIC_NUMBER.put("pptx", new String[]{"504B0304"}); 12 } 13 14 /** 15 * 在檢驗範圍內(MAGIC_NUMBER.keySet(): jpg, gif, png, pdf, xls, ppt, doc, xlsx, pptx, docx) 16 * 且檔案字尾和檔案二進位制頭不一致。返回false 17 */ 18 public static boolean checkFileType(byte[] content, String suffix) { 19 if (!MAGIC_NUMBER.keySet().contains(suffix)) { 20 return true; 21 } 22 23 byte[] bytes = Arrays.copyOfRange(content, 0, Math.min(content.length, MAGIC_HEADER_LENGTH)); 24 String fileCode = getFileHeader(bytes); 25 for (String magicNumber : MAGIC_NUMBER.get(suffix)) { 26 if (fileCode.toUpperCase().startsWith(magicNumber)) { 27 return true; 28 } 29 } 30 return false; 31 }
密碼管理
硬編碼密碼
程式中採用硬編碼方式處理密碼,一方面會降低系統安全性,另一方面不易於程式維護。
1 private String rootManagerPassword = DEFAULTADMINPASSWORD; 2 ...... 3 if (password == null) { 4 password = "123456"; 5 }
修復方式
程式中所需密碼應從配置檔案中獲取經過加密的密碼值。
弱加密
在安全性要求較高的系統中,使用不安全的加密演算法(如DES、RC4、RC5等),將無法保證敏感資料的保密性。
1 Cipher des = Cipher.getInstance("DES"); 2 SecretKey key = KeyGenerator.getInstance("DES").generateKey();
修復方式
使用安全的加密演算法(如AES、3DES、RSA)對敏感資料進行加密。
1 Cipher aes = Cipher.getInstance("AES"); 2 KeyGenerator kg = KeyGenerator.getInstance("AES"); 3 kg.init(128); 4 SecretKey key = kg.generateKey();
不安全的Hash
在安全性要求較高的系統中,不應使用被業界公認的不安全的雜湊演算法(如MD2、MD4、MD5、SHA、SHA1等)來保證資料的完整性。
1 MessageDigest messageDigest = MessageDigest.getInstance("MD5"); 2 messageDigest.update(stringID.getBytes());
修復方式
採用雜湊值>=224位元的SHA系列演算法(如SHA-224、SHA-256、SHA-384和SHA-512)來保證敏感資料的完整性。
1 md = MessageDigest.getInstance("SHA-256"); 2 md.update(bt); 3 strDes = bytes2Hex(md.digest()); // to HexString
不安全的隨機數
JavaAPI中提供了java.util.Random類實現PRNG(),該PRNG是可移植和可重複的,如果兩個java.util.Random類的例項使用相同的種子,會在所有Java實現中生成相同的數值序列。
1 // Random物件r和s設定了相同的種子,因此 i == j 以及陣列b[]和c[]的相應值是相等的。 2 Random r = new Random(12345); 3 int i = r.nextInt(); 4 byte[] b = new byte[4]; 5 r.nextBytes(b); 6 7 Random s = new Random(12345); 8 int j = s.nextInt(); 9 byte[] c = new byte[4]; 10 s.nextBytes(c);
修復方式
使用更安全的隨機數生成器,如java.security.SecureRandom類。
1 SecureRandom number = SecureRandom.getInstance("SHA1PRNG"); 2 System.out.println(number.nextInt() + " " + number.nextInt());
跨站指令碼
XSS
應用程式從資料庫或其它後端資料儲存獲取不可信賴的資料,在未檢驗資料是否存在惡意程式碼的情況下,便將其傳送給了Web使用者,應用程式將易於受到儲存型XSS攻擊。
1 PrintWriter writer = WebUtils.createPrintWriter(res); 2 writer.print(str); 3 writer.flush(); 4 writer.close();
修復方式
對輸出的字串內容進行html轉義編碼.
public static String replaceScript4Xss(String message) { if (StringUtils.isEmpty(message)) { return StringUtils.EMPTY; } StringBuffer builder = new StringBuffer(message.length() * 2); CharacterIterator it = new StringCharacterIterator(message); for (char ch = it.first(); ch != CharacterIterator.DONE; ch = it.next()) { if ((((ch > '`') && (ch < '{')) || ((ch > '@') && (ch < '['))) || (((ch == ' ') || ((ch > '/') && (ch < ':'))) || (((ch == '.') || (ch == ',')) || ((ch == '-') || (ch == '_'))))) { builder.append(ch); } else { builder.append("&#" + (int) ch + ";"); } } return builder.toString(); }
CSRF跨站
跨站請求偽造(CSRF)是偽造客戶端請求的一種攻擊。應用程式允許使用者提交不包含任何保密資訊的請求,將可能導致CSRF攻擊。
如以下程式碼片段用於銀行轉賬功能,若對於該重要敏感的操作沒有進行相應防護,將易於導致跨站請求偽造攻擊。
1 <form method="GET" action="/transferFunds " > 2 cash: <input type="text" name="cash"> 3 to: <input type=" text " name=“to"> 4 <input type="submit" name="action" value="TransferFunds"> 5 </form>
修復方式
(1)二次驗證,進行重要敏感操作時,要求使用者進行二次驗證。
(2)驗證碼,進行重要敏感操作時,加入驗證碼。
(3)在重要敏感操作的表單中加入隱藏的Token,伺服器端程式響應使用者請求前先驗證Token,判斷請求的合法性。
Cookie屬性
Cookie未設定httponly以及secure屬性.
1 Cookie cookie = new Cookie("userName",userName); 2 response.addCookie(cookie);
修復方式
1 Cookie cookie = new Cookie("userName",userName); 2 cookie.setSecure(true); // Secure flag 3 cookie.setHttpOnly(true);
Cookie生命週期
Cookie生命週期不應該超過一年.
1 Cookie cookie = new Cookie("email", email); 2 cookie.setMaxAge(60*60*24*365);
jsessionid
登入前後改變jsessionid標識,修改配置容器, 增強jsessionid演算法邏輯.
1 HttpSession oldSession = req.getSession(false); 2 if (oldSession != null) { 3 //廢棄舊的session, 否則每次退出後再登入, jsessionid不會變化. 4 oldSession.invalidate(); 5 } 6 HttpSession session = req.getSession(true);
SecurityHeaders
配置更安全的HTTP Header
1 res.addHeader("X-Content-Type-Options", "nosniff"); 2 res.addHeader("X-XSS-Protection", "1; mode=block"); 3 res.addHeader("X-Frame-Options", "SAMEORIGIN"); 4 res.addHeader("Content-Security-Policy", "object-src 'self'"); 5 6 res.addHeader("Cache-Control", "no-cache"); 7 res.addHeader("Pragma", "no-cache"); 8 res.addDateHeader("Expires", 0);
資源管理
日期格式化
SimpleDateFormat非執行緒安全的,parse()和format()方法包含一個可導致使用者看到其他使用者資料的racecondition。
1 private static SimpleDateFormat dateFormat;
修復方式
使用ThreadLocal放置SimpleDateFormat或者同步鎖的方式.
訪問許可權
程式未進行恰當的訪問許可權控制,執行了一個包含使用者控制主鍵的SQL語句,可能會導致攻擊者訪問未經授權的記錄。
如下面程式碼片段中的SQL語句用於查詢與指定識別符號相匹配的清單。
1 id = Integer.decode(request.getParameter("invoiceID")); 2 String query = "SELECT * FROM invoices WHERE id = ?"; 3 PreparedStatement stmt = conn.prepareStatement(query); 4 stmt.setInt(1, id); 5 ResultSet results = stmt.execute();
修復方式
先判斷當前使用者許可權是否可以增刪資料, 可以通過把當前被授權的使用者名稱作為查詢語句的一部分來實現。
1 userName = ctx.getAuthenticatedUserName(); 2 id = Integer.decode(request.getParameter("invoiceID")); 3 String query = 4 "SELECT * FROM invoices WHERE id = ? AND user = ?"; 5 PreparedStatement stmt = conn.prepareStatement(query); 6 stmt.setString(1, id); 7 stmt.setString(2, userName); 8 ResultSet results = stmt.execute();
API限流
對於開放的API進行限流操作, 防止資源耗盡.例如獲取IP城市 或者 天氣等等, 限制每個IP每小時最多呼叫1000次之類的.
簡單實現可以用計數器限流, 另外Guava提供了RateLimiter可以實現令牌桶演算法限流.
1 RateLimiter limiter = caches.get(ip); 2 3 if (limiter.tryAcquire()) { 4 System.out.println(i + " success " + new SimpleDateFormat("HH:mm:ss.sss").format(new Date())); 5 } else { 6 System.out.println(i + " failed " + new SimpleDateFormat("HH:mm:ss.sss").format(new Date())); 7 }
資源釋放
對於一些資原始檔, 使用完畢後要在finally語句中進行釋放, 例如connection, 檔案控制代碼, socket等等.
1 try { 2 DatabaseMetaData e = connection.getMetaData(); 3 ResultSet rs1 = e.getTableTypes(); 4 ...... 5 } catch (SQLException e) { 6 return StringUtils.EMPTY; 7 } finally { 8 DBUtils.close(connection); 9 }
路徑遍歷
用程式對使用者可控制的輸入未經合理校驗,就傳送給一個檔案API。攻擊者可能會使用一些特殊的字元(如“..”和“/”)擺脫受保護的限制,訪問一些受保護的檔案或目錄。
1 String path = getInputPath(); 2 if (path.startsWith("/safe_dir/")){ 3 File f = new File(path); 4 f.delete() 5 }
攻擊者可能提供類似下面的輸入:/safe_dir/../important.dat
修復方式
使用白名單策略, 限制資原始檔讀取和訪問.
路徑輸出
禁止輸出伺服器絕對路徑到前端.
1 PrintWriter writer = WebUtils.createPrintWriter(res); 2 writer.write(file.getAbsolutePath());
修復方式
使用相對路徑
資料跨越信任邊界
資料從一個不可信賴域儲存到一個可信賴域導致程式錯誤信賴未驗證的資料。
1 String name = req.getParameter("userName"); 2 HttpSession sess = req.getSession(); 3 sess.setAttribute("user", name);
修復方式
資料跨越信任邊界時需要進行合理的驗證,保證信賴域中資料是安全的。
配置管理
Session失效配置
將Session的失效時間設定為30分鐘或更少,既能使使用者在一段時間內與應用程式互動,又提供了一個限制視窗攻擊的合理範圍。
修復方式
<session-config> <session-timeout>30</session-timeout> </session-config>
錯誤頁面
Web應用程式的預設錯誤頁面不應顯示程式的敏感資訊。Web應用程式應該為4xx(如404)錯誤、5xx(如503)錯誤、java.lang.Throwable異常定義一個錯誤頁面,防止攻擊者挖掘應用程式容器內建錯誤響應資訊。報錯頁面中不應該包含類名, 方法名, 執行堆疊等資訊.
修復方式
應用程式應該在web.xml中配置預設的錯誤頁面。
<error-page> <error-code>403</error-code> <location>/common/403.jsp</location> </error-page> <error-page> <error-code>404</error-code> <location>/common/404.jsp</location> </error-page> <error-page> <error-code>500</error-code> <location>/common/500.jsp</location> </error-page> <error-page> <exception-type>java.lang.Throwable</exception-type> <location>/common/error.jsp</location> </error-page>
不安全的SSLContext
1 SSLContext.getInstance("SSL");
修復方式
配置web容器使用更安全的TLSv1.2協議.
1 SSLContext.getInstance("TLS");
未加密的Socket
1 ServerSocket soc = new ServerSocket(1234); 2 ...... 3 Socket soc = new Socket("www.google.com",80); 4 ......
修復方式
1 ServerSocket soc = SSLServerSocketFactory.getDefault().createServerSocket(1234); 2 ...... 3 Socket soc = SSLSocketFactory.getDefault().createSocket("www.google.com", 443); 4 ......
不安全的FTP協議
程式碼中使用SFTP替代FTP
1 Channel channel = session.openChannel("sftp"); 2 channel.connect();