본문 바로가기
SK shieldus Rookies 16기

[SK shieldus Rookies 16기] 클라우드 기반 스마트 융합 보안 과정 교육 정리(10일차)

by Challenge programmers 2023. 11. 3.

 

 

 

 

오늘 학습 주제

1. 시큐어 코딩 - 보안기능

 

 

 

 

시큐어 코딩 - 보안 기능


- 적절한 인증 없는 중요 기능 허용 -

적절한 인증 없는 중요 기능 허용
정의 보안기능(인증, 접근제어, 기밀성, 암호화, 권한관리 등)을 부적절하게 구현 시
발생할 수 있는 보안 약점
공격 목적 및 발생되는 문제 권한 도용, 주요정보 변조
공격 원리 적절한 인증 없는 중요기능 허용, 부적절한 인가 이용
예방법 • 클라이언트의 보안 검사를 우회하여 서버에 접근하지 못하도록 설계하고
  중요한 정보가 있는 페이지는 재인증을 적용

• 안전하다고 검증된 라이브러리나 프레임워크 (Django authentication system,
  Flask-Login 등)를 사용

 

========================================= 안전하지 않은 코드 =========================================
	new_pwd = request.POST.get('new_password','')
	# 로그인한 사용자 정보
	user = '%s' % escape(request.session['userid'])
	# 현재 password와 일치 여부를 확인하지 않고 수정함
	sha = hashlib.sha256(new_pwd.encode())
	update_password_from_db(user, sha.hexdigest())

=========================================== 안전한 코드 =============================================
	# login_required decorator를 사용해 login된 사용자만 접근하도록 처리
	@login_required
	def change_password(request):
		new_pwd = request.POST.get('new_password','')
		crnt_pwd = request.POST.get('current_password','')
		# 로그인한 사용자 정보를 세션에서 가져온다.
		user = '%s' % escape(request.session['userid'])
		crnt_h = hashlib.sha256(crnt_pwd.encode())
		h_pwd = crnt_h.hexdigest()
		# DB에서 기존 사용자의 Hash된 패스워드 가져오기
		old_pwd = get_password_from_db(user)
		# 패스워드를 변경하기 전 사용자에 대한 재인증을 수행한다.
		if old_pwd == h_pwd:
			new_h = hashlib.sha256(new_pwd.encode())

 

[ 문제점 ]

패스워드 수정 시 수정을 요청한 패스워드와 DB에 저장된 사용자 패스워드 일치 여부를 확인하지 않고 처리

패스워드의 재확인 절차도 생략

 

[ 해결 ]

DB에 저장된 사용자 패스워드와 변경을 요청한 패스워드의 일치 여부를 확인하고,

변경 요청한 패스워드와 재확인 패스워드가 일치하는지 확인 후 DB의 패스워드를 수정

 

 

- 부적절한 인가 -

부적절한 인가
정의 프로그램이 모든 가능한 실행 경로에 대해서 접근 제어를 검사하지 않거나
불완전하게 검사하는 경우 발생할 수 있는 보안 약점
공격 목적 및 발생되는 문제 정보 유출
공격 원리 접근 가능한 실행경로를 통해 정보를 유출
예방법 • 응용 프로그램이 제공하는 정보와 기능이 가지는 역할에 맞게 분리 개발함으로써
  공격자에게 노출되는 공격 노출면(Attack Surface)을 최소화하고 사용자의 권한에
  따른 ACL(Access Control List)을 관리

 

========================================= 안전하지 않은 코드 =========================================
	def delete_content(request):
 		action = request.POST.get('action', '')
	
=========================================== 안전한 코드 =============================================
	@login_required
	# 해당 기능을 수행할 권한이 있는지 확인
	@permission_required('content.delete', raise_exception=True)
		def delete_content(request):

 

[ 문제점 ]

사용자의 권한 확인을 위한 별도의 통제가 적용하지 않음

 

[ 해결 ]

세션에 저장된 사용자 정보를 통해 해당 사용자가 수행할 작업에 대한 권한이 있는지 확인한 후

권한이 있는 경우에만 작업을 수행 

 

 

- 중요한 자원에 대한 잘못된 권한 설정 -

중요한 자원에 대한 잘못된 권한 설정
정의 응용프로그램이 중요한 보안관련 자원에 대해 읽기 또는 수정하기 권한을
의도하지 않게허가할 경우, 권한을 갖지 않은 사용자가 해당 자원을
사용하게 되는 보안 약점
공격 목적 및 발생되는 문제 주요 데이터 접근, 권한 외 자원 사용
공격 원리 사용자 검증 과정이 부적합한 자원에 접근
예방법 • 설정 파일, 실행 파일, 라이브러리 등은 관리자에 의해서만 읽고 쓰기가
  가능하도록 설정

• 설정 파일과 같이 중요한 자원을 사용하는 경우 허가 받지 않은 사용자가
  중요한 자원에 접근 가능한지 검사

 

========================================= 안전하지 않은 코드 =========================================
	# 모든 사용자가 읽기, 쓰기, 실행 권한을 가지게 된다.
	os.chmod('/root/system_config', 0o777)
	
=========================================== 안전한 코드 =============================================
	# 소유자 외에는 아무런 권한을 주지 않음.
	os.chmod('/root/system_config', 0o700)

 

[ 문제점 ]

/root/system_config 파일에 대해서 모든 사용자가 읽기, 쓰기, 실행 권한을 가지는 상황

 

[ 해결 ]

주요 파일에 대해서는 최소 권한만 할당

파일의 소유자라고 하더라도 기본적으로 읽기 권한만 부여해야 하며,

부득이하게 쓰기 권한이 필요한 경우에만 제한적으로 쓰기 권한을 부여

 

 

- 취약한 암호화 알고리즘 사용 -

취약한 암호화 알고리즘 사용
정의 base64, sha123과 같은 취약한 알고리즘 사용시 발생하는 보안 약점
공격 목적 및 발생되는 문제 암호문 무력화
공격 원리 알고리즘을 분석해 무력화
예방법 • 자신만의 암호화 알고리즘을 개발하는 것은 위험

• 학계 및 업계에서 이미 검증된 표준화된 알고리즘을 사용

• DES, RC5 보다는 3TDEA, AES, SEED 등의 안전한 암호알고리즘으로
  대체하여 사용 권고

 

========================================= 안전하지 않은 코드 =========================================
	# 취약함 암호화 알고리즘인 DES를 사용하여 안전하지 않음
	cipher_des = DES.new(key, DES.MODE_ECB)
	
=========================================== 안전한 코드 =============================================
	# 안전한 알고리즘인 AES 를 사용하여 안전함.
	cipher_aes = AES.new(key, AES.MODE_CBC, iv)

 

[ 문제점 ]

취약한 DES 알고리즘으로 암호화

 

[ 해결 ]

취약한 DES 알고리즘 대신 안전한 AES 암호화 알고리즘을 사용

 

 

- 암호화되지 않은 중요정보 -

암호화되지 않은 중요정보
정의 중요 정보가 제대로 보호되지 않을 경우, 보안 문제가 발생하거나
데이터의 무결성이 손상
공격 목적 및 발생되는 문제 정보 유출
공격 원리 시스템의 중요 정보가 포함된 데이터를 평문으로 송·수신 또는 저장 시
인가되지 않은 사용자 에게 민감한 정보가 노출
예방법 • 개인정보(주민등록번호, 여권번호 등), 금융정보(카드번호, 계좌번호 등), 패스워드 등
  중요정보를 저장하거나 통신채널로 전송할 때는 반 드시 암호화 과정을 거쳐야 하며
  중요정보를 읽거나 쓸 경우에 권한인증 등을 통해 적합한 사용자만 중요정보에 접근

• SSL 또는 HTTPS 등과 같은 보안 채널을 사용 (필수)

• 보안 채널을 사용하지 않고 브라우저 쿠키에 중요 데이터를 저장하는 경우,
  쿠키 객체에 보안속성을 설정해(Ex. secure = True) 중요 정보 의 노출을 방지

 

========================================= 안전하지 않은 코드 =========================================
	# 암호화되지 않은 패스워드를 DB에 저장
    curs.execute(
		'UPDATE USERS SET PASSWORD=%s WHERE USER_ID=%s',
		password,
		user_id
	)

=========================================== 안전한 코드 =============================================
	ef update_pass(dbconn, password, user_id, salt):
	# 단방향 암호화를 이용하여 패스워드를 암호화
	hash_obj = SHA256.new()
	hash_obj.update(bytes(password + salt, 'utf-8'))
	hash_pwd = hash_obj.hexdigest()
	curs = dbconn.cursor()
	curs.execute(
		'UPDATE USERS SET PASSWORD=%s WHERE USER_ID=%s',
		(hash_pwd, user_id)
	)

 

[ 문제점 ]

사용자로부터 전달받은 패스워드 암호화 누락

 

[ 해결 ]

해시 알고리즘을 이용하여 단방향 암호화 이후에 패스워드를 저장

 

 

 

- 하드코드된 중요정보 -

암호화되지 않은 중요정보
정의 프로그램 코드 내부에 하드코드된 패스워드를 포함하고, 이를 이용해 내부 인증에
사용하거나 외부 컴포넌트와 통신을 하는 경우 발생하는 보안 약점
공격 목적 및 발생되는 문제 정보 노출
공격 원리 악의적인 사용자가 코드를 열어서 해당 값을 확인
예방법 • 패스워드는 암호화 후 별도의 파일에 저장하여 사용

• 중요 정보 암호화 시 상수가 아닌 암호화 키를 사용

• 암호화가 잘 되었더라도 소스코드 내부에 상수 형태의 암호화 키를 주석으로 달거나
  저장하지 않도록 함

 

========================================= 안전하지 않은 코드 =========================================
	# user, passwd가 소스코드에 평문으로 하드코딩되어 있음
	dbconn = pymysql.connect(
		host='127.0.0.1',
		port='1234',
		user='root',
		passwd='1234',
		db='mydb',
		charset='utf8',
	)

=========================================== 안전한 코드 =============================================
	with open(config_path, 'r') as config:
	# 설정 파일에서 user, passwd를 가져와 사용
		dbconf = json.load(fp=config)
		# 암호화되어 있는 블록 암호화 키를 복호화 해서 가져오는
		# 사용자 정의 함수
		blockKey = get_decrypt_key(dbconf['blockKey'])
		# 설정 파일에 암호화되어 있는 값을 가져와 복호화한 후에 사용
		dbUser = decrypt(blockKey, dbconf['user'])
    	dbPasswd = decrypt(blockKey, dbconf['passwd'])
	dbconn = pymysql.connect(
		host=dbconf['host']
		port=dbconf['port'],
		user=dbUser,
		passwd=dbPasswd,
		db=dbconf['db_name'],
		charset='utf8',
	)

 

[ 문제점 ]

소스코드에 패스워드 또는 암호화 키와 같은 중요 정보를 하드코딩 하는 경우

중요 정보가 노출될 수 있어 위험

 

[ 해결 ]

패스워드와 같은 중요 정보는 안전한 암호화 방식으로 암호화 후 별도의 분리된 공간(파일)에 저장해야 하며

암호화된 정보 사용 시 복호화 과정을 거친 후 사용

암호화된 키는 API로 사용

암호화된 키는 개발자도 알 수 없어야 함

 

 

- 충분하지 않은 키 길이 사용 -

충분하지 않은 키 길이 사용
정의 암호화 및 복호화에 사용되는 키의 길이를 짧게 하여 생기는 보안 약점
공격 목적 및 발생되는 문제 암호화된 데이터와 패스워드의 복호화 유출
공격 원리 공격자가 짧은 시간 안에 키를 찾아 공격
예방법 • RSA 알고리즘은 적어도 2,048 비트 이상의 길이를 가진 키와 함께 사용

• 대칭 암호화 알고리즘(Symmetric Encryption Algorithm)의 경우에는 적어도
  128비트 이상의 키를 사용(암호 강도 112비트 이상)

 

========================================= 안전하지 않은 코드 =========================================
	def make_rsa_key_pair():
		# RSA키 길이를 2048 비트 이하로 설정하는 경우 안전하지 않음
		private_key = RSA.generate(1024)
		public_key = private_key.publickey()

	def make_ecc():
		# ECC의 키 길이를 224비트 이하로 설정하는 경우 안전하지 않음
		ecc_curve = registry.get_curve('secp192r1')

=========================================== 안전한 코드 =============================================
	def make_rsa_key_pair():
		# RSA 키 길이를 2048 비트 이상으로 길게 설정
		private_key = RSA.generate(2048)
		public_key = private_key.publickey()
	def make_ecc():
		# ECC 키 길이를 224 비트 이상으로 설정
		ecc_curve = registry.get_curve('secp224r1')

 

[ 문제점 ]

보안성이 강한 RSA 알고리즘을 사용하는 경우에도

키 사이즈를 작게 설정하면 프로그램의 보안약점 가능

 

[ 해결 ]

RSA, DSA의 경우 키의 길이는 적어도 2048 비트를

ECC의 경우 224 비트 이상으로 설정해야 안전

 

 

- 적절하지 않은 난수 값 사용 -

적절하지 않은 난수 값 사용
정의 예측 가능한 난수를 사용해서 발생하는 보안 약점
공격 목적 및 발생되는 문제 데이터 유출, 권한 획득
공격 원리 난수 발생기를 분석해서 시스템 공격
예방법 • 난수 발생기에서 시드(Seed)를 사용하는 경우에는 고정된 값을 사용하지 않고
  예측하기 어려운 방법으로 생성된 값을 사용하는데 python에서 random 모듈은
  주로 보안 목적이 아닌 게임, 퀴즈 및 시뮬레이션을 위해 설계된 것을
  사용하게 되면 위험

• 세션 ID, 암호화키 등 주요 보안 기능을 위한 값을 생성하고 주요 보안 기능을 수행하는
  경우에는 random 모듈보다 암호화 목적으로 설 계된 secrets 모듈을 사용

 

========================================= 안전하지 않은 코드 =========================================
	# 시스템 현재 시간 값을 시드로 사용하고 있으며, 주요 보안 기능을 위한
	# 난수로 안전하지 않다
	for i in range(6):
		random_str += str(random.randrange(10))

=========================================== 안전한 코드 =============================================
	# 보안기능에 적합한 난수 생성용 secrets 라이브러리 사용
	for i in range(6):
		random_str += str(secrets.randbelow(10))

 

[ 문제점 ]

고정된 seed 값을 보안이나 암호를 목적으로 사용하는 취약한 random 라이브러리 적용

 

[ 해결 ]

secrets 라이브러리를 사용해 6자리의 난수 값을 생성

 

========================================= 안전하지 않은 코드 =========================================
	# random 라이브러리를 보안 기능에 사용하면 위험하다
	return “”.join(random.choice(RANDOM_STRING_CHARS) for i in range(32))

=========================================== 안전한 코드 =============================================
	# 보안 기능과 관련된 난수는 secrets 라이브러리를 사용해야 안전하다
	return “”.join(secrets.choice(RANDOM_STRING_CHARS) for i in range(32))

 

[ 문제점 ]

random 라이브러리를 사용

 

[ 해결 ]

패스워드나 인증정보 및 보안토큰 생성에 사용하는 경우

안전한 secrets 라이브러리로 생성한 난수를 이용

 

 

- 취약한 패스워드 허용 -

취약한 패스워드 허용
정의 사용자에게 강한 패스워드 조합규칙을 요구하지 않아 발생하는 보안 약점
공격 목적 및 발생되는 문제 사용자 계정 탈취
공격 원리 난수 발생기를 분석해서 시스템 공격
예방법 • 패스워드 생성 시 강한 조건 검증을 수행

• 패스워드는 숫자와 영문자, 특수문자 등을 혼합하여 사용하고,
  주기적으로 변경하여 사용

• 안전한 패스워드를 생성하기 위해서는 「패스워드 선택 및 이용 안내서」에서
  제시하는 패스워드 설정 규칙을 적용

 

========================================= 안전하지 않은 코드 =========================================
	if password != confirm_password:
		return make_response("패스워드가 일치하지 않습니다", 400)
	else:
		usertable = User()
		usertable.userid = userid
		usertable.password = password
		# 패스워드 생성 규칙을 확인하지 않고 회원 가입
		db.session.add(usertable)
		db.session.commit()
		return make_response("회원가입 성공", 200)
=========================================== 안전한 코드 =============================================
	if password != confirm_password:
		return make_response("패스워드가 일치하지 않습니다.", 400)
	if not check_password(password):
		return make_response("패스워드 조합규칙에 맞지 않습니다.", 400)
	else:
		usertable = User()
		usertable.userid = userid
		usertable.password = password
		db.session.add(usertable)
		db.session.commit()
		return make_response("회원가입 성공", 200)

	def check_password(password):
		# 3종 이상 문자로 구성된 8자리 이상 패스워드 검사 정규식 적용
		PT1 = re.compile('^(?=.*[A-Z])(?=.*[a-z])[A-Za-z\d!@#$%^&*]{8,}$')
		PT2 = re.compile('^(?=.*[A-Z])(?=.*\d)[A-Za-z\d!@#$%^&*]{8,}$')
		PT3 = re.compile('^(?=.*[A-Z])(?=.*[!@#$%^&*])[A-Za-z\d!@#$%^&*]{8,}$')
		PT4 = re.compile('^(?=.*[a-z])(?=.*\d)[A-Za-z\d!@#$%^&*]{8,}$')
 		PT5 = re.compile('^(?=.*[a-z])(?=.*[!@#$%^&*])[A-Za-z\d!@#$%^&*]{8,}$')
 		PT6 = re.compile('^(?=.*\d)(?=.*[!@#$%^&*])[A-Za-z\d!@#$%^&*]{8,}$')
 		# 문자 구성 상관없이 10자리 이상 패스워드 검사 정규식
 		PT7 = re.compile('^[A-Za-z\d!@#$%^&*]{10,}$')
 		for pattern in [PT1, PT2, PT3, PT4, PT5, PT6, PT7]:
 			if pattern.match(password):
 				return True
 		return False

 

[ 문제점 ]

사용자가 입력한 패스워드에 대한 복잡도 검증 없이 가입 승인 처리를 수행

 

[ 해결 ]

사용자 계정 보호를 위해 회원가입 시 패스워드 복잡도와 길이를 검증 후 가입 승인처리를 수행

코드 내의 특수문자(‘!@#$%^&*’)는 기업 내부 정책에 따라 변경하여 사용하면 되며

패스워드를 숫자로만 10자리로 구성할 경우 취약할 수 있으니

사용자가 안전한 패스워드로 변경할 수 있도록 안내

 

 

- 부적절한 전자서명 확인 -

부적절한 전자서명 확인
정의 프로그램, 라이브러리, 코드의 전자서명에 대한 유효성 검증이 적절하지 않아
공격자의 악의적인 코드가 실행 가능한 보안약점
공격 목적 및 발생되는 문제 데이터 유출, 권한 획득
공격 원리 클라이언트와 서버 사이의 주요 데이터 전송, 파일 다운로드 시
변조된 데이터를 통해 발생
예방법 • 주요 데이터 전송 또는 다운로드 시 데이터에 대한 전자서명을 함께 전송하고
  수신측에서는 전달 받은 전자 서명을 검증해 파일의 변조 여부를 확인

 

========================================= 안전하지 않은 코드 =========================================
	def verify_data(request):
		# 클라이언트로부터 전달받은 데이터(전자서명을 수신 처리 하지 않음)
		encrypted_code = request.POST.get("encrypted_msg", "") # 암호화된 파이썬 코드
		# 서버의 대칭키 로드 (송수신측이 대칭키를 이미 공유했다고 가정)
		with open(f"{PATH}/keys/secret_key.out", "rb") as f:
			secret_key = f.read()
		# 대칭키로 클라이언트가 전달한 파이썬 코드 복호화
		# (decrypt_with_symmetric_key 함수는 임의의 함수명으로 세부적인 복호화 과정은 생략함)
		origin_python_code = decrypt_with_symmetric_key(secret_key, encrypted_code)
		# 클라이언트로부터 전달 받은 파이썬 코드 실행
		eval(origin_python_code)
		return render(
			request,
			"/verify_success.html",
			{"result": "파이썬 코드를 실행했습니다."},
		)

=========================================== 안전한 코드 =============================================
	# 전자서명 검증에 사용한 코드는 의존한 파이썬 패키지 및 송신측 언어에 따라
	# 달라질 수 있으며, 사전에 공유한 공개키로 복호화한 전자서명과 원본 데이터 해시값의
	# 일치 여부를 검사하는 코드를 포함
	def verify_digit_signature (
		origin_data: bytes, origin_signature: bytes, client_pub_key: str ) -> bool:
		hashed_data = SHA256.new(origin_data)
		signer = SIGNATURE_PKCS1_v1_5.new(RSA.importKey(client_pub_key))
		return signer.verify(hashed_data, base64.b64decode(origin_signature))
	
    def verify_data(request):
		# 클라이언트로부터 전달받은 데이터
		encrypted_code = request.POST.get("encrypted_msg", "") # 암호화된 파이썬 코드
		encrypted_sig = request.POST.get("encrypted_sig", "") # 암호화된 전자서명
		# 서버의 대칭(비밀)키 및 공개키 로드
		with open(f"/keys/secret_key.out", "rb") as f:
			secret_key = f.read()
		with open(f"/keys/public_key.out", "rb") as f:
			public_key = f.read()
		# 대칭키로 파이썬 코드 및 전자서명 복호화
		origin_python_code = decrypt_with_symmetric_key(symmetric_key, encrypted_code)
		origin_signature = decrypt_with_symmetric_key(symmetric_key, encrypted_sig)
		# 클라이언트의 공개키를 통해 파이썬 코드(원문)와 전자서명을 검증
		verify_result = verify_digit_signature(origin_python_code, origin_signature, client_pub_key)
		# 전자서명 검증을 통과했다면 파이썬 코드 실행
		if verify_result:
			eval(origin_python_code)
			return render(request, "/verify_success.html",
			{"result": "전자서명 검증 통과 및 파이썬 코드를 실행했습니다."},
			)
		else:
			return render(request, "/verify_failed.html",
 				{"result": "전자서명 또는 파이썬 코드가 위/변조되었습니다."},
 			)

 

[ 문제점 ]

송신측이 데이터와 함께 전달한 전자서명을 수신측에서 별도로 처리하지 않고

데이터를 그대로 신뢰해 데이터 내부에 포함된 파이썬 코드가 실행

 

[ 해결 ]

중요한 정보 또는 기능 실행으로 연결되는 데이터를 전달하는 경우, 반드시 전자서명을 함께 전송해야 하며

수신측에서는 전자서명을 확인해 송신측에서 보낸 데이터의 무결성을 검증

만약 송수신 측 언어가 다른 경우 사용한 암호 라이브러리에 따라 데이터 인코딩 방식에 차이가 있으니

반드시 코드 배포 전 서명 검증에 필요한 복호화 과정이 정상적으로 잘 처리되는지 검증

 

- 부적절한 인증서 유효성 검증 -

부적절한 인증서 유효성 검증
정의 인증서의 유효성 검사가 적절치 않아 발생하는 보안 약점
공격 목적 및 발생되는 문제 정보 유출, 사용자 데이터 무결성 손상
공격 원리 공격자가 호스트와 클라이언트 사이의 통신 구간을 가로채 신뢰하는 엔티티 인 것처럼
속이고 이로 인해 대상 호스트가 신뢰 가능한 것으로 믿고 악성 호스트에 연결하거나
신뢰된 호스트로부터 전달받은 것처럼 보이는 스푸핑 된(또는 변조된 데이터)를 수신  
예방법 • 데이터 통신에 인증서를 사용하는 경우 송신측에서 전달한 인증서가 유효한지
  검증한 후 데이터를 송수신

• 언어에서 기본으로 제공되는 검증 함수가 존재하지 않거나 일반적이지 않은 방식으로
  인증서를 생성한 경우 암호화 패키지를 사용해 별 도의 검증 코드를 작성

 

========================================= 안전하지 않은 코드 =========================================
	HOST, PORT = "127.0.0.1", 7917

	def connect_with_server():
		with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock:
		# 보안 정책 수동 설정
		context = ssl.SSLContext()
		# SSLContext 생성자를 직접 호출할 때, CERT_NONE이 기본값
		# 상대방을 인증하지 않기 때문에 통신하고자하는 서버의 신뢰성을 보장할 수 없음
		context.verify_mode = ssl.CERT_NONE
		with context.wrap_socket(sock) as ssock:
			try:

=========================================== 안전한 코드 =============================================
	CURRENT_PATH = os.getcwd()
	HOST_NAME = "test-server"
	HOST, PORT = "127.0.0.1", 7917
	SERVER_CA_PEM = f"{CURRENT_PATH}/rsa_server/CA.pem" # 서버로부터 전달받은 CA 인증서
	
    def connect_with_server():
		with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock:
			# PROTOCOL_TLS_CLIENT 프로토콜을 추가하여 인증서 유효성 검사와 호스트 이름 확인을 위한
			# context를 구성. verify_mode가 CERT_REQUIRED로 설정됨
			# check_hostname이 True로 설정됨
			context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
			# 서버로부터 전달받은 CA 인증서를 context에 로드
			# CERT_REQUIRED로 인해 필수
			context.load_verify_locations(SERVER_CA_PEM)
			# 호스트 이름(HOST_NAME)이 일치하지 않으면 통신 불가
			# 생성된 소켓과 context wrapping 시 server_hostname이 실제 서버에서
			# 등록(server.csr)한 호스트 명과 일치해야 함
			with context.wrap_socket(sock, server_hostname=HOST_NAME) as ssock:
				try:

 

[ 문제점 ]

클라이언트 측에서 통신 대상 서버를 인증하지 않고 접속하는 상황

서버를 신뢰할 수 없으며 클라이언트 시스템에 영향을 주는 악성 데이터를 수신

 

[ 해결 ]

SSL 연결 시 PROTOCOL_TLS_CLIENT 프로토콜을 추가해 인증서 유효성 검사와

호스트 이름 확인을 위한 context를 구성하면 verify_mode가 CERT_REQUIRED로 설정되며

서버의 인증서 유효성을 검증

 

 

- 사용자 하드디스크에 저장되는 쿠키를 통한 정보 노출 -

사용자 하드디스크에 저장되는 쿠키를 통한 정보 노출
정의 개인정보, 인증 정보 등이 영속적인 쿠키(Persistent Cookie)에 저장되어 발생하는 보안 약점
공격 목적 및 발생되는 문제 정보 유출
공격 원리 쿠키를 탈취해 정보 유출
예방법 • 쿠키의 만료시간은 세션 지속 시간을 고려하여 최소한으로 설정하고
  영속적인 쿠키에는 사용자 권한 등급, 세션 ID 등 중요 정보가 포함하지 않는다

 

========================================= 안전하지 않은 코드 =========================================
	# 쿠키의 만료시간을 1년으로 과도하게 길게 설정하고 있어 안전하지 않다
	res.set_cookie('rememberme', 1, max_age=60*60*24*365)

=========================================== 안전한 코드 =============================================
	# 쿠키의 만료시간을 적절하게 부여하고 secure 및 httpOnly 옵션을 활성화 한다.
	res.set_cookie('rememberme', 1, max_age=60*60, secure=True, httponly=True)

 

[ 문제점 ]

쿠키의 만료시간을 과도하게 길게 설정

 

[ 해결 ]

만료 시간은 해당 기능에 맞춰 최소로 설정

 

 

- 주석문 안에 포함된 시스템 주요 정보 -

주석문 안에 포함된 시스템 주요 정보
정의 주석문에 정보(아이디, 패스워드 등)가 입력되어 발생하는 보안 약점
공격 목적 및 발생되는 문제 의도치 않은 정보 유출
공격 원리 소스코드에 접근 후 주석을 검색하여 정보 획득
예방법 • 주석에는 아이디, 패스워드 등 보안과 관련된 내용을 기입하지 말 것

 

========================================= 안전하지 않은 코드 =========================================
	# 주석문에 포함된 중요 시스템의 인증 정보
	# id = admin
    # passwd = passw0rd
	result = login(id, passwd)

=========================================== 안전한 코드 =============================================
	# 주석문에 포함된 민감한 정보는 삭제
	result = login(id, passwd)

 

[ 문제점 ]

편리성을 위해 아이디, 패스워드 등 중요정보를 주석문 안에 작성

 

[ 해결 ]

프로그램 개발 시에 주석문 등에 남겨놓은 사용자 계정이나 패스워드 등의 정보는 개발 완료 후 확실하게 삭제

 

 

- 솔트 없이 일방향 해시 함수 사용 -

솔트 없이 일방향 해시 함수 사용
정의 중요정보를 저장할 때 무작위 문자열이나 숫자를 넣어준느 솔트(salt)를 사용하지 않아 
발생하는 보안 약점
공격 목적 및 발생되는 문제 해시값 유출
공격 원리 해시값이 미리 계산된 레인보우 테이블을 이용해 해시값 해독
예방법 • 패스워드와 같은 중요 정보를 저장할 경우, 임의의 길이인 데이터를 고정된 크기의
  해시값으로 변환해주는 일방향 해시함수를 이용하여 저장
• 솔트값은 사용자별로 유일하게 생성해야 하며, 이를 위해 사용자별 솔트 값을
  별도로 저장하는 과정이 필요

 

========================================= 안전하지 않은 코드 =========================================
	# salt 없이 생성된 해시값은 강도가 약해 취약하다
	h = hashlib.sha256(pw.encode())

=========================================== 안전한 코드 =============================================
	# 보안기능에 적합한 난수 생성용 secrets 라이브러리 사용
	# 솔트 값을 사용하면 길이가 짧은 패스워드로도 고강도의 해시를 생성할 수 있다.
	# 솔트 값은 사용자별로 유일하게 생성해야 하며, 패스워드와 함께 DB에 저장해야 한다
	salt = secrets.token_hex(32)
	 h = hashlib.sha256(salt.encode() + pw.encode())

 

[ 문제점 ]

salt 없이 길이가 짧은 패스워드를 해시 함수에 전달해 원문이 공격자에 의해 쉽게 유추

 

[ 해결 ]

짧은 길이의 패스워드로 강도 높은 해시값을 생성하기 위해서는 반드시 솔트 값을 함께 전달

 

 

- 무결성 검사 없는 코드 다운로드 -

무결성 검사 없는 코드 다운로드
정의 원격지에 위치한 소스코드 또는 실행 파일을 무결성 검사 없이 다운로드 후
이를 실행하여 발생하는 보안 약점
공격 목적 및 발생되는 문제 데이터 무결성 위협, 악성 코드 삽입, 백도어 생성
공격 원리 호스트 서버의 변조, DNS 스푸핑(Spoofing) 또는 전송 시의 코드 변조 등의 방법을
이용해 공격자가 악의적인 코드를 실행
예방법 • DNS 스푸핑(Spoofing)을 방어할 수 있는 DNS lookup을 수행하고 코드 전송 시
 신뢰할 수 있는 암호 기법을 이용해 코드를 암호화함

• 다운로드한 코드는 작업 수행을 위해 필요한 최소한의 권한으로 실행 하도록 함

• 소스코드는 신뢰할 수 있는 사이트에서만 다운로드해야 하고 파일의 인증서 또는
  해시값을 검사해 변조되지 않은 파일인지 확인하여야 함

 

========================================= 안전하지 않은 코드 =========================================
	# 신뢰할 수 없는 사이트에서 코드를 다운로드
	url = "https://www.somewhere.com/storage/code.py"
	# 원격 코드 다운로드
	file = requests.get(url)
	remote_code = file.content

=========================================== 안전한 코드 =============================================
	config = configparser.RawConfigParser()
	config.read(‘sample_config.cfg’)
	url = "https://www.somewhere.com/storage/code.py"
	remote_code_hash = config.get('HASH', 'file_hash')
	# 원격 코드 다운로드
	file = requests.get(url)
	remote_code = file.content
	sha = hashlib.sha256()
	 sha.update(remote_code)
	# 다운로드 받은 파일의 해시값 검증
	if sha.hexdigest() != remote_code_hash:
		raise Exception(‘파일이 손상되었습니다.’)

 

[ 문제점 ]

원격에서 파일을 다운로드한 뒤 파일에 대한 무결성 검사를 수행하지 않아 파일 변조 등으로 인한 피해가 발생

 

[ 해결 ]

다운로드한 파일과 해당 파일의 해시값 비교 등을 통해 무결성 검사를 거치고 코드를 실행

 

 

- 반복된 인증시도 제한 기능 부재 -

반복된 인증시도 제한 기능 부재
정의 일정 시간 내에 여러 번의 인증 시도 시 계정 잠금 또는 추가 인증 방법 등의
충분한 조치가 수행되지 않아 발생하는 보안 약점
공격 목적 및 발생되는 문제 계정 탈취
공격 원리 공격자는 성공할 법한 계정과 패스워드들을 사전(Dictionary)으로 만들고
무차별 대입(brute-force)하여 로그인 성공 및 권한 획득
예방법 • 최대 인증시도 횟수를 적절한 횟수로 제한하고 설정된 인증 실패 횟수를 초과할 경우
  계정을 잠금 하거나 추가적인 인증 과정을 거쳐서 시스템에 접근이 가능하도록 한다

최대 인증시도 횟수를 적절한 횟수로 제한하고 설정된 인증 실패 횟수를 초과할 경우
  계정을 잠금 하거 나 추가적인 인증 과정을 거쳐서 시스템에 접근이 가능하도록 한다

• 인증시도 횟수를 제한하는 방법 외에 CAPTCHA나 Two-Factor 인증 방법도
  설계 시부터 고려 필요

 

========================================= 안전하지 않은 코드 =========================================
	# 인증 시도에 따른 제한이 없어 반복적인 인증 시도가 가능
	if sha.hexdigest() == hashed_passwd:
		return render(request, '/index.html', {'state':'login_success'})
	else:
		return render(request, '/login.html', {'state':'login_failed'})

=========================================== 안전한 코드 =============================================
	if sha.hexdigest() == hashed_passwd:
		# 로그인 성공 시 실패 횟수 삭제
		LoginFail.objects.filter(user_id=user_id).delete()
		return render(request, '/index.html', {'state':'login_success'})
    # 로그인 실패 기록 가져오기
	if LoginFail.objects.filter(user_id=user_id).exists():
		login_fail = LoginFail.objects.get(user_id=user_id)
		COUNT = login_fail.count
	else:
		COUNT = 0
       
	if COUNT >= LOGIN_TRY_LIMIT:
		# 로그인 실패횟수 초과로 인해 잠금된 계정에 대한 인증 시도 제한
		return render(request, "/account_lock.html", {"state": "account_lock"})
	else:
		# 로그인 실패 횟수 DB 기록
		# 첫 시도라면 DB에 insert,
		# 실패 기록이 존재한다면 update
		LoginFail.objects.update_or_create(
			user_id=user_id,
			defaults={"count": COUNT + 1},
		)

 

[ 문제점 ]

사용자 로그인 시도에 대한 횟수를 제한하지 않는 코드

 

[ 해결 ]

사용자 로그인 시도에 대한 횟수를 제한하여 무차별 공격에 대응