[超長文,謹入]一文解決面試、工作遇到的安全性問題

Ccww發表於2019-09-21

  安全問題其實是很多程式設計師容易忽略的問題但需要我們重視起來,提高應用程式的安全性。常出現的安全問題包括,程式接受資料可能來源於未經驗證的使用者,網路連線和其他不受信任的來源,如果未對程式接受資料進行校驗,則可能會引發安全問題等等,具體也可以分成以下幾方面:

  • 資料校驗
  • 敏感資訊
  • 加密演算法
  • 序列化與反序列化
  • I/O操作
  • 多執行緒安全
  • 框架和元件

  在文章安全開發規範:開發人員必須瞭解開發安全規範(一)(涉及安全問題,以及解決方法和程式碼實現) 中我們闡述了一些關於資料檢驗的安全問題,接下來我們繼續其他部分的安全性問題分析與解決。

資料校驗-許可權管理

規則1.10:禁止程式資料進行增、刪、改、查實對客戶端請求的資料過分相信而遺漏對於許可權的判定

垂直越權漏洞: 稱為許可權提升,是一種“基於URL的訪問控制”設計缺陷引起的漏洞。由於Web應用程式沒有做許可權控制或者僅在選單上做了許可權控制,導致惡意使用者只要猜測其他管理頁面的URL,就可以訪問或控制其他角色擁有的資料或頁面,達到許可權提升的目的。

水平越權漏洞: 一種“基於資料的訪問控制”設計缺陷引起的漏洞。由於伺服器端在接收到請求資料進行操作時沒有判斷資料的所屬人而導致的越權資料訪問漏洞。如伺服器端從客戶端提交的request引數(使用者能夠控制的資料)中獲取使用者id,惡意攻擊者通過變換請求ID的值,檢視或修改不屬於本人的資料。

反例:

 @RequestMapping(value = "delete")
public String delete(HttpServletRequest request, @RequestParam long id) throws Exception{
   try {
       userManage.delete(id);
       request.setAttribute("msg","delete user success");
   }catch (Exception e){
       request.setAttribute("msg","delete user failure");
   }

   return list(request);
}

@RequestMapping(value = "/delete/{addrId}")
public Object remove(@RequestParam long addrId) {
    Map<String,Object> resMap=new HashMap<>();
    if(WebUtils.isLogged){
        this.addressService.removeUserAddress(addrId);
        resMap.put(Constans.RESP_STATUS_CODE_KEY,Constans.RESP_STATUS_CODE_SUCCESS);
        resMap.put(Constans.MESSAGE,"remove user address success");
    }else {
        resMap.put(Constans.RESP_STATUS_CODE_KEY,Constans.RESP_STATUS_CODE_FAIL);
        resMap.put(Constans.MESSAGE,"user is not login ,remove user address failure");
    }
    return resMap;
}
複製程式碼

正例:垂直越權漏洞:在呼叫功能之前,驗證當前使用者身份是否有許可權呼叫相關功能(推薦使用過濾器,進行統一許可權驗證)

 public void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException ,IOException{
    if(request.getSession(true).getAttribute("manager")==null){
        response.sendRedirect("noright.html");
        return;
    }
    UserManagerService userManagerService=new UserManagerService();
    request.setCharacterEncoding("utf-8");
    response.setCharacterEncoding("utf-8");
    String action=request.getParameter("action");
    if("add".equals(action)){
        String id=request.getParameter("userId");
        String name=request.getParameter("userName");
        String sex=request.getParameter("userSex");
    }
    //todo do somethings
}
複製程式碼

資料校驗-許可權管理

  1. 通過全域性過濾器來檢測使用者是否登入,是否對資源具有訪問許可權。
  2. 許可權訪問規則存入資料庫中
  3. web.xml中配置過濾器

public class PriviegeFilter implements Filter{ @Autowired private UserManagerService userManagerService;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        List<UserAuthorization> userAuthorizationS=userManagerService.getUserAuthorizationInfo();
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        for(UserAuthorization userAuthorization: userAuthorizationS){
            // 從資料庫中獲取使用者授權資訊
            if(!authen){
                throw new RuntimeException("您無權訪問頁面,請以合適身份登陸後檢視");
            }
            filterChain.doFilter(servletRequest,servletResponse);
        }
    }

    @Override
    public void destroy() {
        
    }
}
複製程式碼

資料校驗-許可權管理

  1. SpringMVC:Spring Security提供了“基於URL的訪問控制”和“基於Method的訪問控制”。

  2. 在使用者進行操作時,從session中獲取使用者id,將傳入的引數與使用者的身份做繫結校驗。

      <sec:http>
         <sec:intercept-url parttern="/persident_portal/*" access="RILE_PERSIDENT"/>
         <sec:intercept-url parttern="/manager_portal/*" access="RILE_MANAGER"/>
         <sec:intercept-url parttern="/**" access="RILE_USER"/>
         <sec:form-login />
         <sec:logout />
      </sec:http>
    複製程式碼

資料校驗-不安全的網路傳輸

  http協議屬於明文傳輸協議,互動過程以及資料傳輸都沒有進行加密,通訊雙方也沒有進行任何認證,通訊過程非常容易遭遇劫持、監聽、篡改,嚴重情況下,會造成惡意的流量劫持等問題,甚至造成個人隱私洩露(比如銀行卡卡號和密碼洩露)等嚴重的安全問題。
HTTPS在HTTP的基礎上加入了SSL協議,SSL依靠證照來驗證伺服器的身份,併為瀏覽器和伺服器之間的通訊加密。

****對稱加密

[超長文,謹入]一文解決面試、工作遇到的安全性問題

非對稱加密

[超長文,謹入]一文解決面試、工作遇到的安全性問題

https協議(http+ssl協議),如下圖所示為其連線過程:

[超長文,謹入]一文解決面試、工作遇到的安全性問題
中間人攻擊

[超長文,謹入]一文解決面試、工作遇到的安全性問題

數字證照:解決中間人攻擊

  數字證照是一個經證照授權中心數字簽名的包含公開金鑰擁有者資訊以及公開金鑰的檔案 客戶端拿到證照後,根據證照用第三方的私鑰進行上的方法自己生成一個證照編號,如果自己生成的證照編號與證照上的證照編號相同,那 麼說明這個證照是真實的。同時,為避免證照編號本身又被調包,所以使加密。

總結
  HTTPS要使客戶端與伺服器端的通訊過程得到安全保證,必須使用的對稱加密演算法,但是協商對稱加密演算法的過程,需要使用非對稱加密演算法 來保證安全,然而直接使用非對稱加密的過程本身也不安全,會有中間人篡改公鑰的可能性,所以客戶端與伺服器不直接使用公鑰,而是使用 數字證照籤發機構(CA)頒發的證照來保證非對稱加密過程本身的安全,為了保證證照不被篡改,引入數字簽名,客戶端使用相同的對稱加 密演算法,來驗證證照的真實性,如此,最終解決了客戶端與伺服器端之間的通訊安全問題。

資料校驗-不安全的網路傳輸

規則1.11:敏感資料在跨信任域之間傳遞採用簽名加密傳輸

  敏感資料傳輸過程中要防止竊取和惡意篡改。使用安全的加密演算法加密傳輸物件可以保護資料。這就是所謂的對物件進行密封。而對密封的物件進行數字簽名則可以防止物件被非法篡改,保持其完整性

public static void main(String[] args) throwsIOException,ClassNotFoundException{
    // Build map
    SerializableMap<String, Integer> map = buildMap();
    // Serialize map
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data"));
    out.writeObject(map);
    out.close();
    // Deserialize map
    ObjectInputStream in = new ObjectInputStream(new FileInputStream("data"));
    map = (SerializableMap<String, Integer>) in.readObject();
    in.close();
    // Inspect map
    InspectMap(map); 
}
複製程式碼

反例:

public static void main(String[] args) throwsIOException,GeneralSecurityException, ClassNotFoundException{
    // Build map
    SerializableMap<String, Integer> map = buildMap();
    // Generate sealing key & seal map
    KeyGenerator generator = KeyGenerator.getInstance("AES");
    generator.init(newSecureRandom());
    Key key= generator.generateKey();
    Cipher cipher= Cipher.getInstance("AES");
    cipher.init(Cipher.ENCRYPT_MODE, key);
    SealedObject sealedMap= new SealedObject(map, cipher);
    // Serialize map
    ObjectOutputStreamout = new ObjectOutputStream(newFileOutputStream("data"));
    out.writeObject(sealedMap);
    out.close();
    // Deserialize map
    ObjectInputStream in = newObjectInputStream(newFileInputStream("data"));
    sealedMap= (SealedObject) in.readObject();
    in.close();
    // Unseal map
    cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.DECRYPT_MODE, key);
    map = (SerializableMap<String, Integer>) sealedMap.getObject(cipher);
    // Inspect map
    InspectMap(map);

}


public static void main(String[] args) throwsIOException, GeneralSecurityException, ClassNotFoundException{
    SerializableMap<String, Integer> map = buildMap();
    KeyGenerator generator = KeyGenerator.getInstance("AES");
    generator.init(newSecureRandom());
    Key key= generator.generateKey();
    Cipher cipher= Cipher.getInstance("AES");
    cipher.init(Cipher.ENCRYPT_MODE, key);
    SealedObjectsealedMap= newSealedObject(map, cipher);
    KeyPairGeneratorkpg= KeyPairGenerator.getInstance("RSA");
    KeyPair kp= kpg.generateKeyPair();
    Signature sig = Signature.getInstance("SHA256withRSA");
    SignedObject signedMap= newSignedObject(sealedMap, kp.getPrivate(), sig);
    ObjectOutputStreamout = newObjectOutputStream(newFileOutputStream("data"));
    out.writeObject(signedMap);
    out.close();
    ObjectInputStream in = newObjectInputStream(newFileInputStream("data"));
    signedMap= (SignedObject) 
    in.readObject();
    in.close();
    if(!signedMap.verify(kp.getPublic(), sig)){
        throw new GeneralSecurityException("Map failed verification");
    }
    sealedMap= (SealedObject) signedMap.getObject();
    cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.DECRYPT_MODE, key);
    map = (SerializableMap<String, Integer>) sealedMap.getObject(cipher);
    InspectMap(map);

}
複製程式碼

正例:

public static void main(String[] args) throws IOException, GeneralSecurityException, ClassNotFoundException{
    SerializableMap<String, Integer> map = buildMap();
    KeyPairGenerator kpg= KeyPairGenerator.getInstance("RSA");
    KeyPair kp= kpg.generateKeyPair();
    Signature sig = Signature.getInstance("SHA256withRSA");
    SignedObject signedMap= new SignedObject(map, kp.getPrivate(), sig);
    
    KeyGenerator generator = KeyGenerator.getInstance("AES");
    generator.init(new SecureRandom());
    Key key= generator.generateKey();
    Cipher cipher= Cipher.getInstance("AES");
    cipher.init(Cipher.ENCRYPT_MODE, key);
    SealedObject sealedMap = new SealedObject(signedMap, cipher);
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data"));
    out.writeObject(sealedMap);
    out.close();
    // Deserialize map 
    ObjectInputStream in = new ObjectInputStream(new FileInputStream("data")); 
    sealedMap= (SealedObject) in.readObject();
    in.close();
    // Unseal map 
    cipher = Cipher.getInstance("AES"); 
    cipher.init(Cipher.DECRYPT_MODE, key);
    signedMap= (SignedObject) sealedMap.getObject(cipher);
    // Verify signature and retrieve map 
    if(!signedMap.verify(kp.getPublic(), sig)){
        throw new GeneralSecurityException("Map failed verification");
    }
    map = (SerializableMap<String, Integer>) signedMap.getObject();
    // Inspect map 
    InspectMap(map);
}
複製程式碼

敏感資訊

敏感資訊-常見的敏感資訊

[超長文,謹入]一文解決面試、工作遇到的安全性問題

規則2.1:禁止在日誌中明文儲存使用者敏感資料

日誌中如明文儲存使用者敏感資料,容易洩露給運維人員或者攻破系統的攻擊者

規則2.2:禁止將敏感資訊硬編碼在程式中

  如果將敏感資訊(包括口令和加密金鑰)硬編碼在程式中,可能會將敏感資訊暴露給攻擊者。任何能夠訪問到class 檔案的人都可以反編譯class檔案並發現這些敏感資訊

...  
DriverManager.getConnection(url,"soctt","tiger")  
...  
複製程式碼

Java反編譯
javap c Connmngr.class

ldc #36://String jdbc:mysql://ixne.com/rxsql
ldc #38://String scott
ldc #17://String tiger

反例:

public class IPaddress{

private String ipAddress= "172.16.254.1";

	public static voidmain(String[] args){
	//...
	}
}
複製程式碼

正例:

public class IPaddress{

	public static void main(String[] args) throws IOException{
		char[] ipAddress= new char[100];
		BufferedReader br= new BufferedReader(newInputStreamReader(newFileInputStream("serveripaddress.txt")));
		// Reads the server IP address into the char array,
		// returns the number of bytes read 
		intn = br.read(ipAddress);
		// Validate server IP address
		// Manually clear out the server IP address
		// immediately after use 
		for(inti= n -1; i>= 0; i--){
		ipAddress[i] = 0;
		}
		br.close();
	} 
}
複製程式碼

規則2.3:加密傳輸郵件-郵件傳輸時需使用安全協議SSL/TLS加密傳輸,避免攻擊者在網路上嗅探到使用者資料 反例:郵件傳輸時未使用TLS協議

public class SendMailTLS{
	public static void main(String[] args) {
		final String username="username@gmail.com";
		final String password="password";
		Properties props=new Properties();
		//使用TLS
		//props.put("mail.smtp.auth","true");
		//props.put("mail.smtp.startls.enable","true");

		//使用SSL
		//props.put("mail.smtp.socketFactory,port","465");
		//props.put("mail.smtp.socketFactory.class","javax.net.ssl.SSLSocketFactory");
		//props.put("mail.smtp.auth","true");
		
		props.put("mail.smtp.host","smtp.gmail.com");
		props.put("mail.smtp.port","587");
		Session session=Session.getInstance(props,new javax.mail.Authenticator){
			protected PasswordAuthentication getPasswordAuthentication(){
				return new PasswordAuthentication(username,password);
			}
		});

	}
}
複製程式碼

正例:加密使用TLS協議

public class SendMailTLS{
	public static void main(String[] args) {
		final String username="username@gmail.com";
		final String password="password";
		Properties props=new Properties();
		//使用TLS
		props.put("mail.smtp.auth","true");
		props.put("mail.smtp.startls.enable","true");
		//做伺服器證照校驗
		props.put("mail.smtp.ssl.checkserveridentity","true");
		
		//新增信任的伺服器地址,多個地址之間用空格分開
		props.put("mail.smtp.ssl.trust","smtp.gmail.com");
		props.put("mail.smtp.host","smtp.gmail.com");
		props.put("mail.smtp.port","25");
		Session session=Session.getInstance(props,new javax.mail.Authenticator){
			protected PasswordAuthentication getPasswordAuthentication(){
				return new PasswordAuthentication(username,password);
			}
		});

	}
}
複製程式碼

規則2.4:基於hash演算法的口令儲存必須加鹽值(salt),並且使用標準的迭代PBKDF2

如果不加鹽值,會導致相同的密碼得到相同的Hash值

public class PasswordHash{
	public static final String PBKDF2_ALGORITHM="PBKDF2WithHmacSHA1";
	public static final int SALT_BYTE_SIZE=24;
	public static final int HASH_BYTE_SIZE=24;
	public static final int PBKDF2_ITERATIONS=1000;

	public static String createHash(char[] password)
		throws NoSuchAlgorithmException,InvalidKeySpecException{
		
		//Generate a random salt 
		SecureRandom random =new SecureRandom();
		byte[] salt =new btye[SALT_BYTE_SIZE];
		random.nextBytes(salt);
		// Hash the password 
		byte[] hash =pbkdf2(password,salt,PBKDF2_ITERATIONS,HASH_BYTE_SIZE);
		//format iterations:salt:hash
		return PBKDF2_ITERATIONS+":"+toHex(hash);
	
	}
}
複製程式碼

加密演算法

加密演算法-加密演算法總覽

[超長文,謹入]一文解決面試、工作遇到的安全性問題

規則3.1不安全加密演算法-禁止使用不安全的加密演算法DES\3DES

加密演算法應該使用安全的加密演算法AES攻擊者能夠破解不安全的加密演算法,獲取到敏感資訊

反例:使用了不安全演算法DES

byte[] result = DES.encrypt(str.getBytes(),password);
//直接將如上內容解密
try{
byte[] decryResult =DES.decrypt(result,password);
System.out.println("解密後:"+new String(decryResult));
}catch(Excption e1){
e1.printStackTrace();
}
複製程式碼

規則3.3:對稱加密演算法AES-禁止使用AES中不安全的分組模式ECB,推薦使用不僅提供加密並且還提供完整性校驗的AES-GCM

AES分組密碼模式還有:AES-CBC\AES-CFB\AES-OFB\AES-CTR(AES-CTR由於能平行計算,效率最高)

反例:使用了AES中不安全的分組模式ECB

pubilc class AES{
	//加密
	public static String Encrypt(String sSrc,String sKey) throws Excetion{
		if(sKey==null){
			System.out.print("key為空null");
			return null;
		}
		//判斷key是否為16位
		if(key.length()!=16){
			System.out.print("key長度不是16位");
			return null;
		}
		byte[] raw =sKey.getBytes("UTF-8");
		SecreKeySpec skeySpec=new SecreKeySpec(raw,"AES");
		Cipher cipher=Cipher.getInstance("AES/ECB/PKCS5Padding");//演算法/模式/補碼方式
		cipher.init(Cipher.ENCRYPT_MODE,skeySpec);
		byte[] encrypted= cipher.doFinal(sScr.getByte("utf-8"));

		return new Base64().encodeToString(encrypted);//此處使用Base64做轉碼功能,同時能起到2次加密作用
	}

}
複製程式碼

正例:使用AES演算法中安全的分組模式CBC

pubilc class AES{
	//加密
	public static String Encrypt(String sSrc,String sKey) throws Excetion{
		if(sKey==null){
			System.out.print("key為空null");
			return null;
		}
		//判斷key是否為16位
		if(key.length()!=16){
			System.out.print("key長度不是16位");
			return null;
		}
		byte[] raw =sKey.getBytes("UTF-8");
		SecreKeySpec skeySpec=new SecreKeySpec(raw,"AES");
		Cipher cipher=Cipher.getInstance("AES/ECB/PKCS5Padding");//演算法/模式/補碼方式
		IvParameterSpec iv =new IvParameterSpec(sKey.getByte());//使用CBC模式,需要一個向量iv
		//可增加加密演算法的強度
		cipher.init(Cipher.ENCRYPT_MODE,skeySpec,iv);
		byte[] encrypted= cipher.doFinal(sScr.getByte("utf-8"));

		return new Base64().encodeToString(encrypted);//此處使用Base64做轉碼功能,同時能起到2次加密作用
	}

}
複製程式碼

規則3.4:非對稱加密演算法RSA-非對稱加密演算法RSA的使用需要注意長度至少為2048位

RSA的金鑰長度如果低於2048位,達不到安全標準 反例:RSA演算法的金鑰長度只有1024位

public static HashMap<String,Object>getKeys()throws NoSuchAlgorithmException{
	HashMap<String,Object> map=new HashMap<String,Object>;
	KeyPairGenerator keyPairGen=KeyPairGenerator.getInstance("RSA");
	keyPairGen.initialize(1024);
	KeyPair keyPair=keyPairGen.generateKeyPair();
	RSAPublicKey pubilcKey=(RSAPublicKey) keyPair.getPublic();
	RSAPrivateKey privateKey =(RSAPrivateKey) keyPair.getPrivate();
	map.put("public",pubilcKey);
	map.put("private",privateKey);
	return map;
}
複製程式碼

正例:RSA演算法的金鑰長度至少2048位

public static HashMap<String,Object>getKeys()throws NoSuchAlgorithmException{
	HashMap<String,Object> map=new HashMap<String,Object>;
	KeyPairGenerator keyPairGen=KeyPairGenerator.getInstance("RSA");
	keyPairGen.initialize(2048);
	KeyPair keyPair=keyPairGen.generateKeyPair();
	RSAPublicKey pubilcKey=(RSAPublicKey) keyPair.getPublic();
	RSAPrivateKey privateKey =(RSAPrivateKey) keyPair.getPrivate();
	map.put("public",pubilcKey);
	map.put("private",privateKey);
	return map;
}
複製程式碼

規則3.5:敏感資料加密使用強隨機數

  偽隨機數生成器具有可移植性和可重複性,攻擊者可以在系統的一些安全脆弱點上監聽,並構建相應的查詢表預測將要使用的seed值,從而去預測相關的敏感資料
偽隨機數示例:

import java.util.Random;
public class RandomDemo{
    Random random1=new Random(100);
    System.out.println(random1.nextInt());
    System.out.println(random1.nextFloat());
    System.out.println(random1.nextBoolean());
    
    Random random2=new Random(100);
    System.out.println(random2.nextInt());
    System.out.println(random2.nextFloat());
    System.out.println(random2.nextBoolean());
}

import java.io.UnsupporedEncodeingException;
import java.util.Random;

public class SecureRandom{
    public static void main(String[] args)throws UnsupporedEncodeingException{
        Random ranGen=new Random();
        byte[] aesKey=new byte[20];
        ranKey.nextBytes(aesKey);
        StringBuffer hexString =new StringBuffer();
        for (int i=0;i<aesKey.length;i++){
            String hex=Integer.toHexString(0xff&aesKey[i]);
            if(hex.length()==1)
                hexString.append(''0);
            hexString.append(hex);
        }
        System.out.println(hexString);
    }
}
複製程式碼

強隨機數示例:

import java.io.UnsupporedEncodeingException;
import java.util.Random;
import java.security.SecureRandom;

public class SecureRandom{
    public static void main(String[] args)throws UnsupporedEncodeingException{
        Random ranGen=new SecureRandom();
        byte[] aesKey=new byte[20];
        ranKey.nextBytes(aesKey);
        StringBuffer hexString =new StringBuffer();
        for (int i=0;i<aesKey.length;i++){
            String hex=Integer.toHexString(0xff&aesKey[i]);
            if(hex.length()==1)
                hexString.append(''0);
            hexString.append(hex);
        }
        System.out.println(hexString);
    }
}
複製程式碼

序列化與反序列化

  • Java 序列化是指把Java 物件轉換為位元組序列的過程便於儲存在記憶體、檔案、資料庫中,ObjectOutputStream類的writeObject()方法可以實現序列化。

  • Java 反序列化是指把位元組序列恢復為Java 物件的過程,ObjectInputStream 類的readObject() 方法用於反序列化。

      public class Test{
          public static void main(String[] args) throws Exception{
              //定義myObj物件
              MyObject myObj=new MyObject();
              //建立一個包含物件進行反序列化資訊的“object”資料檔案
              FileOutputStream fos=new FileOutputStream("object");
              ObjectOutputStream os =new ObjectOutputStream(fos);
              //writeObject()方法將myObj物件寫入objct檔案中
              os.writeObject(myObj);
              os.close();
              //從檔案中反序列化obj物件
              FileInputStream fis=new FileInputStream("object");
              ObjectInputStream ois =new ObjectInputStream(fis);
              //恢復物件
              MyObject objectFromDisk=(MyObject)ois.readObject();
              System.out.println(objectFromDisk.name);
              ois.close;
          }
      }
    
      class MyObject implements Serializable{
          public String name;
          //重寫readObject()方法
          private void readObject(java.io.ObjectInputStream in) throws IOExeption{
              //執行預設的readObject()方法
              in.defaultReadObject();
              //執行開啟計算器程式命令
              Runtime.getRuntime().exec("open /Application/Calcultor.app")
          }
      }
    複製程式碼

序列化與反序列化-反序列化漏洞

類ObjectInputStream在反序列化時,應對生成的物件的型別做限制

反例:不安全的反序列化操作,導致執行任意命令、控制伺服器

ServerSocket serverSocket =new ServerSocket(Integer,parseInt("9999"));
while(true){
    Socket socket=serverSocket.accpet();
    ObjectInputStream objectInputStream=new ObjectInputStream(socket.getInputStream());
    try{
        Object object=objectInputStream.readObject();
    }catch(Exception e){
        e.printStackTrace();
    }
}
複製程式碼

規則4.1:類ObjectInputStream在反序列化時,對生成的物件的型別做限制

public final  class SecureObjectInputStram extends ObjectInputStream{
    public SecureObjectInputStram() throws IOException{
        super();
    }
    public SecureObjectInputStram( InputStream in) throws IOException{
        super(in);
    }
    portected Class<?>resolveClass(ObjectStreamClass desc) throws ClassNotFoundException,IOException{
        if(!desc.getName().equals("java.security.Person")){
            throws new ClassNotFoundException(desc.getName()+"not found");
        }
        return super.resolveClass(desc);
    }
}
複製程式碼

序列化與反序列化-不安全的反序列化漏洞解決方案

解決方案:

  • 使用安全的反序列化外掛,對於業界爆出的反序列化漏洞,及時更新外掛和打補丁。
  • 傳輸的資料應該加密,並且在後臺進行資料校驗,保證資料沒有被篡改。
  • 自定義ObjectInputStream, 過載resolveClass的方法,對className進行白名單校驗。
  • Java9可以繼承java.io.ObjectInputFilter類重寫checkInput方法實現自定義的過濾器。
  • 通過擴充套件SecurityManager 來實現禁止JVM 執行外部命令Runtime.exec。

I/O操作

規則5.1:檔案上傳應根據業務的需要限定檔案的格式和大小

反例:未限定格式

@RequestMapping(value="/upload",method=RequestMethod.POST)
public String upload(HttpServletRequest request, @RequestParam("description") String description,
                    @RequestParam("file") MultipartFile file)throws Exception{
            if(!file.isEmpty()){
             String path=request.getServletContext().getRealPath("/images/");
            //上傳檔名
             String filename=file.getOriginalFilename();
            File filepath=new File(path,filename);
             //判斷路徑是否存在,如果不存在就建立一個
            if(!filepath.getParentFile().exists()){
                   filepath.getParentFile.mkdirs();
            }
            //將檔案上傳儲存到一個目標檔案中
            file.transferTo(new File(path+File.separator+filename));
                return "success"
            }else{
                return "error";
             }
          }
複製程式碼

正例:限定檔案的上傳的格式

@RequestMapping(value="/upload",method=RequestMethod.POST)
public String upload(HttpServletRequest request, @RequestParam("description") String description,
                    @RequestParam("file") MultipartFile file)throws Exception{
        if(!file.isEmpty()){
            String path=request.getServletContext().getRealPath("/images/");
             //上傳檔名
            String filename=file.getOriginalFilename();
            //還有一種方式filenameUtils.getExtension();
            String suffix=filename.substring(filename.lastIndexOf(".")+1)
            if(suffix!="jpg"){
                   File filepath=new File(path,filename);
                   //判斷路徑是否存在,如果不存在就建立一個
                   if(!filepath.getParentFile().exists()){
                            filepath.getParentFile.mkdirs();
                        }
                    //將檔案上傳儲存到一個目標檔案中
                     file.transferTo(new File(path+File.separator+filename));
                    }
                return "success" ;
            }else{
                return "error";
           }
       }
複製程式碼

規則5.2:路徑遍歷-檔案下載的地方,應對檔案的路徑進行校驗,或者使用檔案id對映到檔案的方式下載檔案

反例:

protected void doGet(HttpServletRequest request,HttpServeltResponse response)
        throws ServeltExceptio,IOException{
        //獲取專案部署絕對路徑下的upload資料夾路徑,下載upload目錄下的檔案
        String root =request.getServeltContext().getRealPath("/upload");
        //獲取檔名
        String filename=request.getParameter("filename");
        //根據檔案路徑建立輸入流
        File file=new File(root+"/"+filename);
        FileInputStream fis= new FileInputStream(file);
        //設定響應頭
        response.addHeader("Content-Disposition","attachment;filename="+new String(filename.getBytes()));
        response.addHeader("Content-Length",""+file.length());
        byte[] b =new byte[fis.availabe()];
        fis.read(b);
        response.getOutStream().write(b);
        }
複製程式碼

正例:使用白名單校驗檔名

protected void doGet(HttpServletRequest request,HttpServeltResponse response)
        throws ServeltExceptio,IOException{
        //獲取專案部署絕對路徑下的upload資料夾路徑,下載upload目錄下的檔案
        String root =request.getServeltContext().getRealPath("/upload");
        //獲取檔名
        String filename=request.getParameter("filename");
        if(filename==FILENAME){
               //根據檔案路徑建立輸入流
            File file=new File(root+"/"+filename);
            FileInputStream fis= new FileInputStream(file);
            //設定響應頭
            response.addHeader("Content-Disposition","attachment;filename="+new String(filename.getBytes()));
            response.addHeader("Content-Length",""+file.length());
            byte[] b =new byte[fis.availabe()];
            fis.read(b);
            response.getOutStream().write(b);  
             }
        }
複製程式碼

規則5.3:未釋放的流資源(Unreleased Resource,比如Streams使用檔案、IO流、資料庫連線等)主動釋放的資源

  使用檔案、IO流、資料庫連線等不會自動釋放的資源時,未在使用完畢後馬上將其關閉,關閉資源的程式碼應在try...catch...finally{if (fos != null) {fos.close();}}的finally內執行

private static void TestCloseFileStream(){
    String fileName="";
    //宣告引用
    InputStream inputStream=null;
    try{
        inputStream=new FileInputStream(filename);
    }catch(IOException e){
        //do something
    }finally{
        if(inputStream!=null){
            try{
                //關閉流
              inputStream.close();  
            }catch(IOException e){
                //do something
            }
        }
    }
}
複製程式碼

規則5.4:臨時檔案使用完畢應及時刪除

反例:

public class TempFile{
    public static void main(STring[] args) throws IOExcption{
        File f =new File("tempnam.tmp");
        if(f.exists()){
            System.out.println("This file already exists");
            return;
        }
        FileOutputStream fop=null;
        try{
            fop=new FileOutputStream(f);
            String str="Data";
            fop.write(str.getBytes());
        }finally{
            if(fop!=null){
                try{
                    fop.close();
                }catch(IOException e){
                    // handle error
                }
            }
        }
    }
}
複製程式碼

正例:

public class TempFile{
    public static void main(STring[] args) throws IOExcption{
        File f =new File("tempnam.tmp");
        if(f.exists()){
            System.out.println("This file already exists");
            return;
        }
        FileOutputStream fop=null;
        try{
            fop=new FileOutputStream(f);
            String str="Data";
            fop.write(str.getBytes());
        }finally{
            if(fop!=null){
                try{
                    fop.close();
                }catch(IOException e){
                    // handle error
                }
                //delete file when finished
                if(!f.delete()){
                    //log the error
                }
            }
        }
    }
}
複製程式碼

多執行緒安全(Double-Checked Locking)

規則6.1:多執行緒下采用加鎖機制,防止多執行緒操作產生的死鎖

雙重鎖機制(多執行緒下不安全)

public class Singleton{
    private static Singleton singleton;
    private Singleton(){}
    public static Singleton getSingleton(){
        if(singleton==null){
            synchronized (Singleton.class){
                if(singleton==null){
                    singleton=new Singleton();
                }
            }
        }
        return singleton;
    }
}


singleton = new Singleton()非原子性  
1. memory=allocate();//1:分配物件的記憶體空間  
2. ctorInstance(memory);//2:初始化物件  
3. singleton=memory;//3:設定instance指向剛分配的記憶體地址  
指令重排後:  
1. memory=allocate();//1:分配物件的記憶體空間  
2. singleton=memory;//3:設定instance指向剛分配的記憶體地址//注意,此時物件還沒有被初始化!  
3. ctorInstance(memory);//2:初始化物件  
複製程式碼

多執行緒安全(Double-Checked Locking)
對方法使用synchronized關鍵字

 public class Singleton{
    private static Singleton singleton;
    private Singleton(){}
    public static synchronized Singleton getSingleton(){
        if(singleton==null){
            singleton=new Singleton();
            }
        return singleton;
        }
}
複製程式碼

提前初始化

 public class Singleton{
     private static class SingletonHolder{
          private static final Singleton singleton=new Singleton();
     }
    private Singleton(){}
    public static final Singleton getSingleton(){
        return SingletonHolder.INSTANCE;
        }
}
複製程式碼

框架和元件-框架和元件安全

規則7.1:使用安全版本的框架和元件,官網下載,使用框架和元件,先到網上搜掃下是否有公開的漏洞

比如: --WebLogic、Struts2、Nginx --Fastjson、ImageMagick、Log4j

規則7.2:禁止使用來源不明的框架或者元件

規則7.3:及時更新框架和元件版本

異常行為-異常資訊暴露到外部

規則8.1:異常資訊禁止暴露到前端

try{
    FileInputStream fis =new FileInputStream(System.getenv("APPDATA")+args[0]);
}catch(FileNotFoundException e){
    //Log the exception
    throws new IOException ("Unable to retrieve file",e);
}
複製程式碼

解析:產生固定的錯誤資訊,未洩露異常資訊到外部


最後可關注公眾號,一起學習,每天會分享乾貨,還有學習視訊領取!

[超長文,謹入]一文解決面試、工作遇到的安全性問題

相關文章