1. 程式人生 > >JAVA 程式碼安全規範

JAVA 程式碼安全規範

申明:本文非筆者原創,原文轉載自:https://github.com/SecurityPaper/SecurityPaper-web/blob/master/_posts/2.SDL%E8%A7%84%E8%8C%83%E6%96%87%E6%A1%A3/2018-08-17-SDL-3-java%E5%AE%89%E5%85%A8%E7%BC%96%E7%A0%81%E8%A7%84%E8%8C%83.md

 

1輸入驗證和資料合法性校驗
程式接受資料可能來源於未經驗證的使用者,網路連線和其他不受信任的來源,如果未對程式接受資料進行校驗,則可能會引發安全問題。

1.1避免SQL注入
1.1.1  使用PreparedStatement

預編譯SQL,解決SQL注入問題,傳遞給PreparedStatement物件的引數可以被強制進行型別轉換,確保在插入或查詢資料時與底層的資料庫格式匹配。 

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();

1.1.2 Mybatis 使用#{ },相當於使用PreparedStatement

1.1.3 採用正則表示式將包含有 單引號('),分號(;) 和 註釋符號(--)的語句給替換掉來防止SQL注入。還要小心 (OR)等敏感詞彙。sql.replaceAll(".*([';]+|(--)+).*", " ");


1.2避免XML注入
通過StringBulider 或 StringBuffer 拼接XML檔案時,需對輸入資料進行合法性校驗。 對數量quantity 進行合法性校驗,控制只能傳入0-9的數字:

if (!Pattern.matches("[0-9]+", quantity)) {
    // Format violation
  }
  String xmlString = "<item>\n<description>Widget</description>\n" +
                     "<price>500</price>\n" +
                     "<quantity>" + quantity + "</quantity></item>";
  outStream.write(xmlString.getBytes());
  outStream.flush();


1.3避免跨站點指令碼(XSS)

web專案通過Filter +HttpServletRequestWrapper  過濾


對產生跨站的引數進行嚴格過濾,禁止傳入<SCRIPT>標籤

//定義需過濾的欄位串<script>

String s = "\uFE64" + "script" + "\uFE65";

// 過濾字串標準化

s = Normalizer.normalize(s, Form.NFKC);

// 使用正則表示式匹配inputStr是否存在<script>

Pattern pattern = Pattern.compile(inputStr);
Matcher matcher = pattern.matcher(s);
if (matcher.find()) {
  // Found black listed tag
  throw new IllegalStateException();
} else {
  // ...
}

1.4 CSRF跨站請求偽造

https://blog.csdn.net/starrrr2/article/details/50074445

https://www.cnblogs.com/zhufu9426/p/7814084.html

下面是CSRF的常見特性: 
1. 依靠使用者標識危害網站 
2. 利用網站對使用者標識的信任 
3. 欺騙使用者的瀏覽器傳送HTTP請求給目標站點 
4. 可以通過IMG標籤會觸發一個GET請求,可以利用它來實現CSRF攻擊

一個簡單的例子: 
* 同一瀏覽器使用者小z登入了網站A,同時開啟網站B 
* 網站B隱蔽的傳送一個請求至網站A 
* 網站A通過session、cookie等身份標記判斷是使用者小z,執行對應操作

這樣網站B內的非法程式碼就盜用了使用者小z的身份,在小z不知情的情況下執行了攻擊者需要的操作,這就是跨站請求偽造。
防禦CSRF可以通過動態token驗證的方式來實現,每次請求生成一個動態token給前端,前端在後續的請求中附加該token,如果token不存在或不正確說明不是正常請求,予以遮蔽,從而達到解決CSRF問題的目的,以下是具體實現。


2宣告和初始化
2.1避免類初始化的相互依賴
例:

錯誤的寫法:

public class Cycle {
  private final int balance;
  private static final Cycle c = new Cycle();
  private static final int deposit = (int) (Math.random() * 100); // Random deposit
  public Cycle() {
    balance = deposit - 10; // Subtract processing fee
  }
  public static void main(String[] args) {
    System.out.println("The account balance is: " + c.balance);
  }
}
類載入時初始化指向Cycle類的靜態變數c,而類Cycle的無參構造方法又依賴靜態變數deposit,導致無法預期的結果。 正確的寫法:

public class Cycle {
  private final int balance;
  private static final int deposit = (int) (Math.random() * 100); // Random deposit
  private static final Cycle c = new Cycle();  // Inserted after initialization of required fields
  public Cycle() {
    balance = deposit - 10; // Subtract processing fee
  }
 
  public static void main(String[] args) {
    System.out.println("The account balance is: " + c.balance);
  }
}


3表示式
3.1不可忽略方法的返回值

忽略方法的放回值可能會導致無法預料的結果。

錯誤的寫法:

public void deleteFile(){
  File someFile = new File("someFileName.txt");
   someFile.delete();
}
正確的寫法:

public void deleteFile(){
  File someFile = new File("someFileName.txt");
   if (!someFile.delete()) {
    // handle failure to delete the file
  }
}
3.2不要引用空指標
當一個變數指向一個NULL值,使用這個變數的時候又沒有檢查,這時會導致。NullPointerException。

在使用變數前一定要做是否為NULL值的校驗。

Object obj = getObject();
if (obj != null){
 obj.toString();
}

3.3使用Arrays.equals()來比較陣列的內容
陣列沒有覆蓋的Object. equals()方法,呼叫Object. equals()方法實際上是比較陣列的引用,而不是他們的內容。程式必須使用兩個引數Arrays.equals()方法來比較兩個陣列的內容

public void arrayEqualsExample() {
  int[] arr1 = new int[20]; // initialized to 0
  int[] arr2 = new int[20]; // initialized to 0
  Arrays.equals(arr1, arr2); // true
}


4數字型別和操作
4.1防止整數溢位

使用java.lang.Number. BigInteger類進行整數運算,防止整數溢位。

基本操作 add,subtract,multiply,divide,compareTo

public class BigIntegerUtil {

    private static final BigInteger bigMaxInt = BigInteger.valueOf(Integer.MAX_VALUE);
    private static final BigInteger bigMinInt = BigInteger.valueOf(Integer.MIN_VALUE);

    public static BigInteger intRangeCheck(BigInteger val) throws ArithmeticException {
        if (val.compareTo(bigMaxInt) == 1 || val.compareTo(bigMinInt) == -1) {
            throw new ArithmeticException("Integer overflow");
        }
        return val;
    }

    public static int addInt(int v1, int v2) throws ArithmeticException {
        BigInteger b1 = BigInteger.valueOf(v1);
        BigInteger b2 = BigInteger.valueOf(v2);
        BigInteger res = intRangeCheck(b1.add(b2));
        return res.intValue()
    }
    
    public static int subInt(int v1, int v2) throws ArithmeticException {
        BigInteger b1 = BigInteger.valueOf(v1);
        BigInteger b2 = BigInteger.valueOf(v2);
        BigInteger res = intRangeCheck(b1.subtract(b2));
        return res.intValue(); 
    }
    
    public static int multiplyInt(int v1, int v2) throws ArithmeticException {
        BigInteger b1 = BigInteger.valueOf(v1);
        BigInteger b2 = BigInteger.valueOf(v2);
        BigInteger res = intRangeCheck(b1.multiply(b2));
        return res.intValue(); 
    }
    
    public static int divideInt(int v1, int v2) throws ArithmeticException {
        BigInteger b1 = BigInteger.valueOf(v1);
        BigInteger b2 = BigInteger.valueOf(v2);
        BigInteger res = intRangeCheck(b1.divide(b2));
        return res.intValue(); 
    }
}
4.2避免除法和取模運算分母為零
要避免因為分母為零而導致除法和取模運算出現異常。

if (num2 == 0) {
  // handle error
} else {
 result1= num1 /num2;
  result2= num1 % num2;
}


5類和方法操作
5.1資料成員宣告為私有,提供可訪問的包裝方法

攻擊者可以用意想不到的方式操縱public或protected的資料成員,所以需要將資料成員為private,對外提供可控的包裝方法訪問資料成員。

5.2敏感類不允許複製
包含私人的,機密或其他敏感資料的類是不允許被複制的,解決的方法有兩種:

1、類宣告為final

final class SensitiveClass {
  // ...
}
2、Clone 方法丟擲CloneNotSupportedException異常

class SensitiveClass {
  // ...
  public final SensitiveClass clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException();
  }
}
5.3比較類的正確做法
如果由同一個類裝載器裝載,它們具有相同的完全限定名稱,則它們是兩個相同的類。 不正確寫法:

// Determine whether object auth has required/expected class object
 if (auth.getClass().getName().equals(
      "com.application.auth.DefaultAuthenticationHandler")) {
   // ...
}
正確寫法:
// Determine whether object auth has required/expected class name
 if (auth.getClass() == com.application.auth.DefaultAuthenticationHandler.class) {
   // ...
}


5.4不要硬編碼敏感資訊
硬編碼的敏感資訊,如密碼,伺服器IP地址和加密金鑰,可能會洩露給攻擊者。

敏感資訊均必須存在在配置檔案或資料庫中。

5.5驗證方法引數
驗證方法的引數,可確保操作方法的引數產生有效的結果。不驗證方法的引數可能會導致不正確的計算,執行時異常,違反類的不變數,物件的狀態不一致。 對於跨信任邊界接收引數的方法,必須進行引數合法性校驗

private Object myState = null;
//對於修改myState 方法的入參,進行非空和合法性校驗
void setState(Object state) {
  if (state == null) {
    // Handle null state
  }
  if (isInvalidState(state)) {
    // Handle invalid state
  }

 if ( instanceof) {

  }
  myState = state;
}


5.6不要使用過時、陳舊或低效的方法
在程式程式碼中使用過時的、陳舊的或低效的類或方法可能會導致錯誤的行為。

5.7陣列引用問題
某個方法返回一個對敏感物件的內部陣列的引用,假定該方法的呼叫程式不改變這些物件。即使陣列物件本身是不可改變的,也可以在陣列物件以外運算元組的內容,這種操作將反映在返回該陣列的物件中。如果該方法返回可改變的物件,外部實體可以改變在那個類中宣告的 public 變數,這種改變將反映在實際物件中。

不正確的寫法:

public class XXX {
    private String[] xxxx;
    public String[] getXXX() {
            return xxxx;
    }
}
正確的寫法:

public class XXX {
    private String[] xxxx;
    public String[] getXXX()             
            return  Arrays.copyof(…);  // 或其他陣列複製方法
    }
}


5.8不要產生記憶體洩露
垃圾收集器只收集不可達的物件,因此,存在未使用的可到達的物件,仍然表示記憶體管理不善。過度的記憶體洩漏可能會導致記憶體耗盡,拒絕服務(DoS)。

6異常處理
6.1不要忽略捕獲的異常

對於捕獲的異常要進行相應的處理,不能忽略已捕獲的異常

不正確寫法:

class Foo implements Runnable {
  public void run() {
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      // 此處InterruptedException被忽略
    }
  }
}
正確寫法:

class Foo implements Runnable {
  public void run() {
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt(); // Reset interrupted status
    }
  }
}


6.2不允許暴露異常的敏感資訊
沒有過濾敏感資訊的異常堆疊往往會導致資訊洩漏,

不正確的寫法:

try {
  FileInputStream fis =
      new FileInputStream(System.getenv("APPDATA") + args[0]);
} catch (FileNotFoundException e) {
  // Log the exception
  throw new IOException("Unable to retrieve file", e);
}
正確的寫法:

class ExceptionExample {
  public static void main(String[] args) {
    File file = null;
    try {
      file = new File(System.getenv("APPDATA") +
             args[0]).getCanonicalFile();
      if (!file.getPath().startsWith("c:\\homepath")) {
        log.error("Invalid file");
        return;
      }
    } catch (IOException x) {
     log.error("Invalid file");
      return;
    }
    try {
      FileInputStream fis = new FileInputStream(file);
    } catch (FileNotFoundException x) {
      log.error("Invalid file");
      return;
    }
  }
}


6.3不允許丟擲RuntimeException, Exception,Throwable
不正確的寫法:

boolean isCapitalized(String s) {
  if (s == null) {
    throw new RuntimeException("Null String");
  }
}

private void doSomething() throws Exception {
  //...
}
正確寫法:

boolean isCapitalized(String s) {
  if (s == null) {
    throw new NullPointerException();
  }
}

private void doSomething() throws IOException {
  //...
}


6.4不要捕獲NullPointerException或其他父類異常
不正確的寫法:

boolean isName(String s) {
  try {
    String names[] = s.split(" ");
    if (names.length != 2) {
      return false;
    }
    return (isCapitalized(names[0]) && isCapitalized(names[1]));
  } catch (NullPointerException e) {
    return false;
  }
}
正確的寫法:

boolean isName(String s) /* throws NullPointerException */ {
  String names[] = s.split(" ");
  if (names.length != 2) {
    return false;
  }
  return (isCapitalized(names[0]) && isCapitalized(names[1]));
}


7多執行緒程式設計
7.1確保共享變數的可見性

對於共享變數,要確保一個執行緒對它的改動對其他執行緒是可見的。 執行緒可能會看到一個陳舊的共享變數的值。為了共享變數是最新的,可以將變數宣告為volatile或lock同步讀取和寫入操作。 將共享變數宣告為volatile:

final class ControlledStop implements Runnable {
  private volatile boolean done = false;
  @Override public void run() {
    while (!done) {
      try {
        // ...
        Thread.currentThread().sleep(1000); // Do something
      } catch(InterruptedException ie) { 
        Thread.currentThread().interrupt(); // Reset interrupted status
      } 
    }    
  }
  public void shutdown() {
    done = true;
  }
}
同步讀取和寫入操作:

final class ControlledStop implements Runnable {
  private boolean done = false;
  @Override public void run() {
    while (!isDone()) {
      try {
        // ...
        Thread.currentThread().sleep(1000); // Do something
      } catch(InterruptedException ie) { 
        Thread.currentThread().interrupt(); // Reset interrupted status
      } 
    }    
  }
  public synchronized boolean isDone() {
    return done;
  }
  public synchronized void shutdown() {
    done = true;
  }
}


7.2確保共享變數的操作是原子的
除了要確保共享變數的更新對其他執行緒可見的,還需要確保對共享變數的操作是原子的,這時將共享變數宣告為volatile往往是不夠的。需要使用同步機制或Lock 同步讀取和寫入操作:

final class Flag {
  private volatile boolean flag = true;
  public synchronized void toggle() {
    flag ^= true; // Same as flag = !flag;
  }
  public boolean getFlag() {
    return flag;
  }
}
//使用讀取鎖確保讀取和寫入操作的原子性

final class Flag {
  private boolean flag = true;
  private final ReadWriteLock lock = new ReentrantReadWriteLock();
  private final Lock readLock = lock.readLock();
  private final Lock writeLock = lock.writeLock();
  public void toggle() {
    writeLock.lock();
    try {
      flag ^= true; // Same as flag = !flag;
    } finally {
      writeLock.unlock();
    }
  }
  public boolean getFlag() {
    readLock.lock();
    try {
      return flag;
    } finally {
      readLock.unlock();
    }
  }
}


7.3不要呼叫Thread.run(),不要使用Thread.stop()以終止執行緒
7.4確保執行阻塞操作的執行緒可以終止

  public final class SocketReader implements Runnable {
  private final SocketChannel sc;
  private final Object lock = new Object();
  public SocketReader(String host, int port) throws IOException {
    sc = SocketChannel.open(new InetSocketAddress(host, port));
  }
  @Override public void run() {
    ByteBuffer buf = ByteBuffer.allocate(1024);
    try {
      synchronized (lock) {
        while (!Thread.interrupted()) {
          sc.read(buf);
          // ...
        }

      }
    } catch (IOException ie) {
      // Forward to handler
    }
  }
  public static void main(String[] args) 
                          throws IOException, InterruptedException {
    SocketReader reader = new SocketReader("somehost", 25);
    Thread thread = new Thread(reader);
    thread.start();
    Thread.sleep(1000);
    thread.interrupt();
  }
}


7.5相互依存的任務不要在一個有限的執行緒池執行
有限執行緒池指定可以同時執行線上程池中的執行緒數量的上限。程式不得使用有限執行緒池執行緒執行相互依賴的任務。可能會導致執行緒飢餓死鎖,所有的執行緒池執行的任務正在等待一個可用的執行緒中執行一個內部佇列阻塞,或是相關任務被拒絕的風險

7.6 鎖放在事務之外

當鎖放在事務之內時,會造成多個執行緒同時啟動事務,然後在等待鎖,由於事物的隔離性,可能會造成大範圍幻讀

8輸入輸出
8.1程式終止前刪除臨時檔案
8.2檢測和處理檔案相關的錯誤

Java的檔案操作方法往往有一個返回值,而不是丟擲一個異常,表示失敗。因此,忽略返回值檔案操作的程式,往往無法檢測到這些操作是否失敗。Java程式必須檢查執行檔案I / O方法的返回值。

不正確的寫法:

File file = new File(args[0]);
file.delete();
正確的寫法:
File file = new File("file");
if (!file.delete()) {
  log.error("Deletion failed");
}
8.3及時釋放資源
垃圾收集器無法釋放非記憶體資源,如開啟的檔案描述符與資料庫的連線。因此,不釋放資源,可能導致資源耗盡攻擊。

try {
  final FileInputStream stream = new FileInputStream(fileName);
  try {
    final BufferedReader bufRead =
        new BufferedReader(new InputStreamReader(stream));
 
    String line;
    while ((line = bufRead.readLine()) != null) {
      sendLine(line);
    }
  } finally {
    if (stream != null) {
      try {
        stream.close();
      } catch (IOException e) {
        // forward to handler
      }
    }

  }
} catch (IOException e) {
  // forward to handler
}


9序列化
9.1不要序列化未加密的敏感資料

序列化允許一個物件的狀態被儲存為一個位元組序列,然後重新在稍後的時間恢復,它沒有提供任何機制來保護序列化的資料。敏感的資料不應該被序列化的例子包括加密金鑰,數字證書。 解決方法:

對於資料成員可以使用transient ,宣告該資料成員是瞬態的。
重寫序列化相關方法writeObject、readObject、readObjectNoData,防止被子類惡意重寫
class SensitiveClass extends Number {
  // ...
  protected final Object writeObject(java.io.ObjectOutputStream out) throws NotSerializableException {
    throw new NotSerializableException();
  }
  protected final Object readObject(java.io.ObjectInputStream in) throws NotSerializableException {
    throw new NotSerializableException();
  }
  protected final Object readObjectNoData(java.io.ObjectInputStream in) throws NotSerializableException {
    throw new NotSerializableException();
  }
}


9.2在序列化過程中避免記憶體和資源洩漏
不正確的寫法:

class SensorData implements Serializable {
  // 1 MB of data per instance!
   public static SensorData readSensorData() {...}
  public static boolean isAvailable() {...}
}
class SerializeSensorData {
  public static void main(String[] args) throws IOException {
    ObjectOutputStream out = null;
    try {
      out = new ObjectOutputStream(
          new BufferedOutputStream(new FileOutputStream("ser.dat")));
      while (SensorData.isAvailable()) {
        // note that each SensorData object is 1 MB in size
        SensorData sd = SensorData.readSensorData();
        out.writeObject(sd);
      }
    } finally {
      if (out != null) {
        out.close();
      }
    }
  }
}
正確寫法:

class SerializeSensorData {
  public static void main(String[] args) throws IOException {
    ObjectOutputStream out = null;
    try {
      out = new ObjectOutputStream(
          new BufferedOutputStream(new FileOutputStream("ser.dat")));
      while (SensorData.isAvailable()) {
        // note that each SensorData object is 1 MB in size
        SensorData sd = SensorData.readSensorData();
        out.writeObject(sd);
        out.reset(); // reset the stream
      }
    } finally {
      if (out != null) {
        out.close();
      }
    }
  }
}
9.3反序列化要在程式最小許可權的安全環境中