Java 프레임워크 만들기 - JSP

자바 데이터베이스 매니저 시큐어 코딩 - Java Database Manager Secure Coding (PreparedStatement), 이클립스(Eclipse)

carrotweb 2021. 6. 13. 19:26
728x90
반응형

이전 자바 데이터베이스 매니저 만들기(https://carrotweb.tistory.com/70)에서 쿼리(query)문 실행을 위해서 "createStatement"매소드를 이용하여 정적인 쿼리(query)문을 사용하였습니다. 이전에도 언급한 것처럼 정적인 쿼리(query)문은 "SQL Injection"으로 보안상 취약점이 있습니다.

실제로 "SQL Injection"를 테스트 해보고 안정적인 코딩을 위해 "PreparedStatement"를 이용하여 쿼리(query)문을 실행 할 수 있게 하겠습니다.

"JAVA 시큐어코딩 가이드"의 "제1절 입력 데이터 검증 및 표현"에서 "1. SQL 삽입(Improper Neutralization of Special Elements used in an SQL Command, SQL Injection)"부분입니다.

로그인에서 아이디와 패스워드만으로 쿼리하고 입력 값에 대한 검증(비교) 없이 로그인 섹션을 생성하는 시스템에 대한 취약점을 보여줍니다. 만약, 아이디와 패스워드만으로 쿼리한다면 검증(비교) 부분을 추가해야 합니다.

1. "Servers"탭에서 "tomcat8"를 선택하고 "start"버튼(start the server)을 클릭합니다. 웹 브라우저에서 "http://localhost:8080/test2/testform.do"를 입력합니다.

아이디에는 "guest"를 패스워드에는 "xx' OR 1=1 --"를 입력하고 "로그인"버튼을 클릭합니다.

"Console"탭을 아이디만으로 쿼리한 것을 확인할 수 있고 아이디가 "guest"인 것만 쿼리합니다.

SELECT MBR_ID, MBR_PWD, MBR_PWD_SALT, MBR_NM FROM MBR_ACCOUNT_TB WHERE MBR_ID = 'guest'

만약 아이디가 "guest"가 있다고 해도 로그인 서비스에서 쿼리문 실행 후 입력된 아이디와 패스워드를 검증(비교)하기 때문에 로그인이 되지 않았습니다.

이처럼 쿼리문 실행후 검증(비교) 단계가 필요합니다. 그렇지만 보안상 취약한 것은 마찬가지입니다.

2. 다시 아이디에는 "guest' OR 1=1 --"를 패스워드에는 "123456"를 입력하고 "로그인"버튼을 클릭합니다.

"Console"탭을 보면 쿼리문 구문 오류가 발생합니다. 이유는 입력한 아이디("guest' OR 1=1 --")에 '''(싱글 따움표)가 포함되있어 쿼리문에 3개의 '''(싱글 따움표)로 인해 오류가 발생합니다. '''(싱글 따움표)으로 인한 구문 오류임으로 입력한 아이디 끝에 '''(싱글 따움표)를 추가해 주면 해결됩니다.

 

id : guest' OR 1=1 --, password : 123456
데이터베이스에 연결됨
Statement를 생성함
Query[SELECT MBR_ID, MBR_PWD, MBR_PWD_SALT, MBR_NM FROM MBR_ACCOUNT_TB WHERE MBR_ID = 'guest' OR 1=1 --']를 실행하지 못함
java.sql.SQLSyntaxErrorException: (conn=36) You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ''' at line 1

다시 아이디 끝에 싱글 따움표(')를 추가하고 "guest' OR 1=1 --'"를 패스워드에는 "123456"를 입력하고 "로그인"버튼을 클릭합니다.

"Console"탭을 보면 에러 없이 쿼리문은 실행됩니다.

SELECT MBR_ID, MBR_PWD, MBR_PWD_SALT, MBR_NM FROM MBR_ACCOUNT_TB WHERE MBR_ID = 'guest' OR 1=1 --''

문제는 쿼리문에 "OR 1=1 --'"으로 인해 회원 전체가 쿼리되어 데이터베이스에 부하를 줍니다. 그리고 쿼리된 ROW중 첫번째가 리턴되고 입력된 아이디와 패스워드로 검증합니다. 로그인은 되지 않았지만 데이터베이스에 부하를 주어 시스템에 문제가 발생할 수 있습니다.

이처럼 "SQL Injection"으로 인해 시스템에 문제가 발생됩니다. 그래서 쿼리(query)문을 "Statement" 대신 "PreparedStatement"를 이용하면 외부 입력 문자열이 쿼리문의 구조를 바꾸는 것을 방지할 수 있습니다.

3. "TestLoginDaoImpl.java"의 "selectMember"메소드에서 쿼리문 실행 구문을 주석처리합니다.

//String query = "SELECT MBR_ID, MBR_PWD, MBR_PWD_SALT, MBR_NM FROM MBR_ACCOUNT_TB WHERE MBR_ID = '" + userVO.getId() + "'";
//ResultSet resultSet = testDatabaseManager.executeQuery(query);
try {
	if (resultSet.next()) {
		selectUserVO = new UserVO();
		String memberID = resultSet.getString("MBR_ID");
		String memberPassword = resultSet.getString("MBR_PWD");
		String memberPasswordSalt = resultSet.getString("MBR_PWD_SALT");
		String memberName = resultSet.getString("MBR_NM");
		selectUserVO.setId(memberID);
		selectUserVO.setPassword(memberPassword);
		selectUserVO.setPasswordSalt(memberPasswordSalt);
		selectUserVO.setName(memberName);
	} else {
		System.out.println("resultSet이 없음");
	}
} catch (SQLException e) {
	System.out.println("resultSet를 가져오지 못함");
	e.printStackTrace();
}

쿼리(query)문을 "PreparedStatement"에서 인식할 수 있게 변경합니다.

String query = "SELECT MBR_ID, MBR_PWD, MBR_PWD_SALT, MBR_NM FROM MBR_ACCOUNT_TB WHERE MBR_ID=?";

"DriverManager"의 "getConnection"메소드를 실행하여 데이터베이스에 연결합니다.

Connection connection = testDatabaseManager.getConnection();
if (connection != null) {
	testDatabaseManager.closeConnection(connection);
}

"PreparedStatement"문을 생성하고 아이디를 첫번째 파라메타값으로 설정합니다.

PreparedStatement preparedStatement = null;
try {
	preparedStatement = connection.prepareStatement(query);
	preparedStatement.setString(1, userVO.getId());
	System.out.println("PreparedStatement를 생성함");
} catch (SQLException e) {
	System.out.println("PreparedStatement를 생성하지 못함");
	e.printStackTrace();
}

 

 

"executeQuery"메소드를 통해 쿼리문을 실행하게 합니다.

ResultSet resultSet = null;
try {
	resultSet = preparedStatement.executeQuery();
	System.out.println("Query[" + preparedStatement.toString() + "]를 실행함");
} catch (SQLException e) {
	System.out.println("Query[" + preparedStatement.toString() + "]를 실행하지 못함");
	e.printStackTrace();
} finally {
	try {
		preparedStatement.close();
		System.out.println("PreparedStatement를 종료함");
	} catch (SQLException e) {
		System.out.println("PreparedStatement를 종료하지 못함");
		e.printStackTrace();
	}
}

전체 소스입니다.

//String query = "SELECT MBR_ID, MBR_PWD, MBR_PWD_SALT, MBR_NM FROM MBR_ACCOUNT_TB WHERE MBR_ID = '" + userVO.getId() + "'";
//ResultSet resultSet = testDatabaseManager.executeQuery(query);
String query = "SELECT MBR_ID, MBR_PWD, MBR_PWD_SALT, MBR_NM FROM MBR_ACCOUNT_TB WHERE MBR_ID=?";
Connection connection = testDatabaseManager.getConnection();
if (connection != null) {
	PreparedStatement preparedStatement = null;
	try {
		preparedStatement = connection.prepareStatement(query);
		preparedStatement.setString(1, userVO.getId());
		System.out.println("PreparedStatement를 생성함");
	} catch (SQLException e) {
		System.out.println("PreparedStatement를 생성하지 못함");
		e.printStackTrace();
	}

	if (preparedStatement != null) {
		ResultSet resultSet = null;
		try {
			resultSet = preparedStatement.executeQuery();
			System.out.println("Query[" + preparedStatement.toString() + "]를 실행함");
		} catch (SQLException e) {
			System.out.println("Query[" + preparedStatement.toString() + "]를 실행하지 못함");
			e.printStackTrace();
		}

		try {
			if (resultSet.next()) {
				selectUserVO = new UserVO();
				String memberID = resultSet.getString("MBR_ID");
				String memberPassword = resultSet.getString("MBR_PWD");
				String memberPasswordSalt = resultSet.getString("MBR_PWD_SALT");
				String memberName = resultSet.getString("MBR_NM");
				selectUserVO.setId(memberID);
				selectUserVO.setPassword(memberPassword);
				selectUserVO.setPasswordSalt(memberPasswordSalt);
				selectUserVO.setName(memberName);
			} else {
				System.out.println("resultSet이 없음");
			}
		} catch (SQLException e) {
			System.out.println("resultSet를 가져오지 못함");
			e.printStackTrace();
		}

		try {
			preparedStatement.close();
			System.out.println("PreparedStatement를 종료함");
		} catch (SQLException e) {
			System.out.println("PreparedStatement를 종료하지 못함");
			e.printStackTrace();
		}
	}
	testDatabaseManager.closeConnection(connection);
}

 

4. "Servers"탭에서 "tomcat8"를 선택하고 "start"버튼(start the server)을 클릭합니다. 이전과 같이 아이디에는 "guest' OR 1=1 --'"를 패스워드에는 "123456"를 입력하고 "로그인"버튼을 클릭합니다.

아이디에 "guest' OR 1=1 --'"은 입력된 문자열로 들어가 쿼리문의 구조가 바뀌지 않은 것을 확인할 수 있습니다.

 

[Console]

id : guest' OR 1=1 --', password : 123456
데이터베이스에 연결됨
PreparedStatement를 생성함
Query[sql : 'SELECT MBR_ID, MBR_PWD, MBR_PWD_SALT, MBR_NM FROM MBR_ACCOUNT_TB WHERE MBR_ID=?', parameters : ['guest' OR 1=1 --'']]를 실행함
PreparedStatement를 종료함
resultSet이 없음
데이터베이스에서 연결을 종료함
ServletRequest에 속성 추가 - 속성명 : errorMessage, 속성 값 : 아이디/패스워드가 정확하지 않습니다.

 

아이디에는 "testid"를 패스워드에는 "testpwd"를 입력하고 "로그인"버튼을 클릭합니다.

"Console"탭을 보면 정상적으로 로그인되어 "http://localhost:8080/test2/testform.do"이 나타납니다.

[Console]

id : testid, password : testpwd
데이터베이스에 연결됨
PreparedStatement를 생성함
Query[sql : 'SELECT MBR_ID, MBR_PWD, MBR_PWD_SALT, MBR_NM FROM MBR_ACCOUNT_TB WHERE MBR_ID=?', parameters : ['testid']]를 실행함
PreparedStatement를 종료함
데이터베이스에서 연결을 종료함
HttpSession에 속성 추가 - 속성명 : auth, 속성 값 : com.home.project.test2.vo.UserVO@62a44067
Session Manager Count : 1, Add ID : testid, Session ID : F70056300D8C53CCCB680636E4A05B68
ServletRequest에 속성 추가 - 속성명 : errorMessage, 속성 값 : 
returnUrl : /test2/testform.do
728x90
반응형