java安全編碼指南之:輸入注入injection

flydean發表於2020-10-12

簡介

注入問題是安全中一個非常常見的問題,今天我們來探討一下java中的SQL隱碼攻擊和XML注入的防範。

SQL隱碼攻擊

什麼是SQL隱碼攻擊呢?

SQL隱碼攻擊的意思是,使用者輸入了某些引數,最終導致SQL的執行偏離了程式設計者的本意,從而導致越權或者其他型別的錯誤。

也就是說因為使用者輸入的原因,導致SQL的涵義傳送了變化。

拿我們最常用的登入的SQL語句來說,我們可能會寫下面的SQL語句:

select * from user where username='<username>' and password='<password>'

我們需要使用者傳入username和password。

怎麼對這個SQL語句進行注入呢?

很簡單,當使用者的username輸入是下面的情況時:

somebody' or '1'='1

那麼整個SQL語句將會變成:

select * from user where username='somebody' or '1'='1' and password='<password>'

如果somebody是一個有效的使用者,那麼or後面的語言完全不會執行,最終導致不校驗密碼就返回了使用者的資訊。

同樣的,惡意攻擊者可以給password輸入下面的內容可以得到同樣的結果:

' or '1'='1

整個SQL解析為:

select * from user where username='somebody' and password='' or '1'='1'

這條語句將會返回所有的使用者資訊,這樣即使不知道確定存在的使用者名稱也可以通過SQL語句的判斷。

這就是SQL隱碼攻擊。

java中的SQL隱碼攻擊

java中最常用的就是通過JDBC來運算元據庫,我們使用JDBC建立好連線之後,就可以執行SQL語句了。

下面我們看一個java中使用JDBC SQL隱碼攻擊的例子。

先建立一個通用的JDBC連線:

    public Connection getConnection() throws ClassNotFoundException, SQLException {
        Connection con = null;
            Class.forName("com.mysql.jdbc.Driver");
            System.out.println("資料庫驅動載入成功");
            con = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mysql?characterEncoding=UTF-8", "root", "");
            System.out.println("資料庫連線成功");
          return con;
    }

然後再自己拼裝SQL語句然後呼叫:

public void jdbcWithInjection(String username,char[] password) throws SQLException, ClassNotFoundException {
        Connection connection = getConnection();
        if (connection == null) {
            // Handle error
        }
        try {
            String pwd = encodePassword(password);

            String sqlString = "SELECT * FROM user WHERE username = '"
                    + username +
                    "' AND password = '" + pwd + "'";
            Statement stmt = connection.createStatement();
            ResultSet rs = stmt.executeQuery(sqlString);

            if (!rs.next()) {
                throw new SecurityException(
                        "User name or password incorrect"
                );
            }
        } finally {
            try {
                connection.close();
            } catch (SQLException x) {
            }
        }
    }

上面的例子中,只有username會發生注入,password不會,因為我們使用了encodePassword方法對password進行了轉換:

public String encodePassword(char[] password){
        return Base64.getEncoder().encodeToString(new String(password).getBytes());
    }

使用PreparedStatement

為了防止SQL隱碼攻擊,我們一般推薦的是使用PreparedStatement,java.sql.PreparedStatement可對輸入引數進行轉義,從而防止SQL隱碼攻擊。

注意,一定要正確的使用PreparedStatement,如果是不正確的使用,同樣會造成SQL隱碼攻擊的結果。

下面看一個不正確使用的例子:

String sqlString = "SELECT * FROM user WHERE username = '"
                    + username +
                    "' AND password = '" + pwd + "'";
            PreparedStatement stmt = connection.prepareStatement(sqlString);
            ResultSet rs = stmt.executeQuery();

上面的程式碼中,我們還是自己進行了SQL的拼裝,雖然最後我們使用了preparedStatement,但是沒有達到效果。

正確使用的例子如下:

String sqlString =
                    "select * from user where username=? and password=?";
            PreparedStatement stmt = connection.prepareStatement(sqlString);
            stmt.setString(1, username);
            stmt.setString(2, pwd);
            ResultSet rs = stmt.executeQuery();

我們需要將使用者輸入作為引數set到PreparedStatement中去,這樣才會進行轉義。

XML中的SQL隱碼攻擊

可擴充套件標記語言(XML)旨在幫助儲存,結構化和傳輸資料。 由於其平臺獨立性,靈活性和相對簡單性,XML已在許多應用程式中得到使用。 但是,由於XML的多功能性,它容易受到包括XML注入在內的各種攻擊的攻擊。

那麼什麼是XML注入呢?我們舉個例子:

<item>
  <name>Iphone20</name>
  <price>5000.0</price>
  <quantity>1</quantity>
</item>

上面的例子中,我們使用了XML定義了一個iphone20的價格和數量。一個iphone20 5000塊。

上面的XML中,如果quantity是使用者輸入的資料的話,那麼使用者可以這樣輸入:

1</quantity><price>20.0</price><quantity>1

最後得出的XML檔案如下:

<item>
  <name>Iphone20</name>
  <price>5000.0</price>
  <quantity>1</quantity>
  <price>20.0</price><quantity>1</quantity>
</item>

一般來說,我們在解析XML的過程中,如果發現有重複的tag,那麼後面的tag會覆蓋前面的tag。

結果就是1個iphone20現在的價格是20塊,非常划算。

XML注入的java程式碼

我們看下XML的注入在java程式碼中是怎麼實現的:

    public String createXMLInjection(String quantity){
        String xmlString = "<item>\n<name>Iphone20</name>\n"
                + "<price>5000.0</price>\n" + "<quantity>" + quantity
                + "</quantity></item>";
        return xmlString;
    }

可以看到我們直接使用使用者輸入的quantity作為XML的拼接,這樣做很明顯是有問題的。

怎麼解決呢?有兩種方法。

  • 第一種方法

第一種方法就是對使用者輸入的quantity進行校驗:

    public String createXML(String quantity){
        int count = Integer.parseUnsignedInt(quantity);
        String xmlString = "<item>\n<name>Iphone20</name>\n"
                + "<price>5000.0</price>\n" + "<quantity>" + count
                + "</quantity></item>";
        return xmlString;
    }

上面程式碼中,我們對quantity進行了Integer的轉換,從而避免了使用者的非法輸入。

  • 第二種方法

第二種方法是使用XML Schema,來對生成的XML進行格式校驗。

先看一下我們改怎麼定義這個XML Schema:


<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="item">
  <xs:complexType>
    <xs:sequence>
      <xs:element name="name" type="xs:string"/>
      <xs:element name="price" type="xs:decimal"/>
      <xs:element name="quantity" type="xs:nonNegativeInteger"/>
    </xs:sequence>
  </xs:complexType>
</xs:element>
</xs:schema>

上面我們定義了一個XML element的序列sequence。如果使用者輸入了非定義格式的其他XML,就會報錯。

我們看下相對應的java程式碼該怎麼寫:

StreamSource ss = new StreamSource(new File("schema.xsd"));
            Schema schema = sf.newSchema(ss);
            SAXParserFactory spf = SAXParserFactory.newInstance();
            spf.setSchema(schema);
            SAXParser saxParser = spf.newSAXParser();
            XMLReader reader = saxParser.getXMLReader();
            reader.setContentHandler(defHandler);
            reader.parse(xmlStream);

上面我們列出了XML驗證的程式碼,完整的程式碼可以參考文末的程式碼連結,這裡就不一一貼出來了。

本文的程式碼:

learn-java-base-9-to-20/tree/master/security

本文已收錄於 http://www.flydean.com/java-security-code-line-injection/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!

相關文章