https://popcorn-overflow.tistory.com/15
앞서 기본적인 응답 주고받기 수준의 기능을 구현하였습니다.
하지만 여기서 보안적 이슈가 등장하는데요,
저희는 슬랙 내부에서 봇을 사용하고 있는 유저와 그렇지 않은 유저들을 구분하고, 그렇지 않은 유저들의 접근을 차단해야 합니다.
사실 public하게 이용하는 서비스라면 해당 과정은 생략해도 될 것 같네요.
간단하게 구현해보겠습니다.
"설명 필요없고 나는 동작하는 파일이 필요해"
-> app.py
import hashlib
import hmac
import time
import os
import json
from flask import Flask, request, make_response, abort
app = Flask(__name__)
SIGNING_SECRET = os.environ.get('SLACK_SIGNING_SECRET')
@app.before_request
def before_request():
# Check only for routes that require Slack verification
if not verify_slack_request(request):
abort(403)
def verify_slack_request(request):
# 1. Get Slack request headers
slack_signature = request.headers.get('X-Slack-Signature', '')
slack_request_timestamp = request.headers.get('X-Slack-Request-Timestamp', '')
# 2. Check timestamp
if abs(time.time() - float(slack_request_timestamp)) > 60 * 5:
# The request timestamp is older than five minutes
return False
# 3. Create a string based on the request
basestring = f"v0:{slack_request_timestamp}:".encode('utf-8') + request.get_data()
# 4. Generate a signature using HMAC SHA256
my_signature = 'v0=' + hmac.new(
SIGNING_SECRET.encode('utf-8'),
basestring,
hashlib.sha256
).hexdigest()
# 5. Compare the generated signature with Slack's signature
if hmac.compare_digest(my_signature, slack_signature):
return True
return False
@app.route('/', methods=['POST'])
def hello_world():
slack_event = json.loads(request.data)
if "challenge" in slack_event:
return make_response(slack_event["challenge"], 200, {"content_type": "application/json"})
return make_response("There are no slack request events", 404, {"X-Slack-No-Retry": 1})
@app.route('/hello', methods=['POST'])
def hello():
return make_response("안녕 친구야", 200, {"content_type": "application/json"})
if __name__ == '__main__':
app.run()
이거 쓰시면 잘 될겁니다. 하단에 설명드리겠습니다.
목표
- 컨트롤러로 요청이 넘어가기 전에, 외부 요청을 차단하는 필터를 만든다.
- 슬랙 API에 있는 Signing Secret을 이용, 슬랙으로부터 오는 요청을 validate 한다.
Signing Secret 확인하기
Basic Information -> 하단으로 내려 App Credentials의 Signing Secret을 확인 후 서버에 담아둡니다.
민감한 정보이므로 환경변수에 담는 것을 추천합니다. (로컬에서 개발중이신 경우 꼭 그럴 필요는 없습니다.)
SIGNING_SECRET = os.environ.get('SLACK_SIGNING_SECRET')
.env에서 환경변수 읽어오는 방법에 대해선 생략합니다.
app.py 코드 작성
verifier
def verify_slack_request(request):
# 1. Get Slack request headers
slack_signature = request.headers.get('X-Slack-Signature', '')
slack_request_timestamp = request.headers.get('X-Slack-Request-Timestamp', '')
# 2. Check timestamp
if abs(time.time() - float(slack_request_timestamp)) > 60 * 5:
# The request timestamp is older than five minutes
return False
# 3. Create a string based on the request
basestring = f"v0:{slack_request_timestamp}:".encode('utf-8') + request.get_data()
# 4. Generate a signature using HMAC SHA256
my_signature = 'v0=' + hmac.new(
SIGNING_SECRET.encode('utf-8'),
basestring,
hashlib.sha256
).hexdigest()
# 5. Compare the generated signature with Slack's signature
if hmac.compare_digest(my_signature, slack_signature):
return True
return False
슬랙에서 오는 요청인지 확인합니다.
# 1. Get Slack request headers
slack_signature = request.headers.get('X-Slack-Signature', '')
slack_request_timestamp = request.headers.get('X-Slack-Request-Timestamp', '')
슬랙 -> 서버로 오는 요청의 헤더에서 Signature, Timestamp를 가져옵니다.
# 2. Check timestamp
if abs(time.time() - float(slack_request_timestamp)) > 60 * 5:
# The request timestamp is older than five minutes
return False
timestamp을 체크하여 5분이 지났는지 확인합니다.
>> 5분이 지난 요청은 재전송이거나, 탈취된 요청 등의 위험이 있다고 간주하여 폐기합니다.
# 3. Create a string based on the request
basestring = f"v0:{slack_request_timestamp}:".encode('utf-8') + request.get_data()
시크릿 키, 타임스탬프 및 요청 본문을 합쳐 문자열을 생성합니다.
# 4. Generate a signature using HMAC SHA256
my_signature = 'v0=' + hmac.new(
SIGNING_SECRET.encode('utf-8'),
basestring,
hashlib.sha256
).hexdigest()
이 문자열에 HMAC SHA256을 적용하여 서명을 생성합니다.
>> 여기서 사용되는 SIGNING_SECRET이 아까 담아두었던 환경변수입니다.
본문이 같고, Timestamp가 같고, Signing Secret이 같습니다.
그렇다면 요청의 헤더에 있던 X-Slack-Signature와 my_signatures는 일치할 것을 기대할 수 있습니다.
# 5. Compare the generated signature with Slack's signature
if hmac.compare_digest(my_signature, slack_signature):
return True
return False
비교 후 같으면 정상적 요청이므로 True, 아니면 False를 반환합니다.
이것으로 validate 함수를 만들었습니다.
이제 모든 요청에서 해당 함수가 실행되게 설정하면 됩니다.
컨트롤러마다 직접 함수를 실행시켜 False면 튕기게 설정할 수 있지만, filter를 만들겠습니다.
filter
@app.before_request
def before_request():
# Check only for routes that require Slack verification
if not verify_slack_request(request):
abort(403)
verify_slack_request 함수를 실행, False면 403(Forbidden)으로 abort합니다.
PostMan으로 테스트 시 403 에러가 뜨는 것을 확인 가능합니다.
'Server' 카테고리의 다른 글
Github Actions: ssh-aciton IPv6 지원 (1) | 2024.03.17 |
---|---|
[Terraform] state locking을 위한 s3, ddb 설정하기 (0) | 2024.02.23 |
[서버] Github Actions: 다양한 명령 내리기 (0) | 2023.11.29 |
[Docker, AWS, Spring] docker-compose시 환경변수 전달하고 스프링부트 application.yaml에서 사용하기 (3) | 2023.10.06 |
[Slack API] 슬랙 API를 통한 요청 가이드 (1) (0) | 2023.08.16 |