diff --git a/ecs/jskult-webapp/README.md b/ecs/jskult-webapp/README.md index 97632b8f..c65cbf83 100644 --- a/ecs/jskult-webapp/README.md +++ b/ecs/jskult-webapp/README.md @@ -84,6 +84,8 @@ │   ├── exception_handler.py -- FastAPI内部でエラー発生時のハンドリング │   └── exceptions.py -- カスタム例外クラス ├── main.py -- APサーバーのエントリーポイント。ここでルーターやハンドラーの登録を行う + ├── middleware -- ミドルウェアの設定 + │ └── middleware.py ├── model -- モデル層(MVCのM) │   ├── db -- リポジトリから返されるDBレコードのモデル │   │   ├── base_db_model.py @@ -195,3 +197,39 @@ - コントローラーのrouter変数が、`router.route_class = Authenticate`となっている場合、以下の動きをする - リクエスト到達時にセッションの有無をチェックする - レスポンス時、クッキーにセッションキーを登録する + +## HTMLで読み込んでいるスクリプトのSRIハッシュ値を生成・設定する方法 + +### サブリソース完全性 (Subresource Integrity, SRI) とは + +CDN などから取得したリソースが意図せず改ざんされていないかをブラウザーが検証するセキュリティ機能です。 SRI を利用する際には、取得したリソースのハッシュ値と一致すべきハッシュ値を指定します。 + +詳細: + +実消化&アルトマークのWebアプリケーションでは、複数の外部スクリプトを読み込んで動作しているため、読み込むスクリプトを変更した場合は、 +タグの属性値`integrity`に設定されているスクリプトのハッシュ値を更新する必要がある。 + +### SRI ハッシュ値の生成方法(サーバー内のスクリプトについて) + +- サーバー内に保管されているスクリプトを更新した場合、Linux環境(WSL2でも可)で、以下のコマンドを実行し、ハッシュ値を生成する + +```bash +cat <更新したスクリプトファイル名> | openssl dgst -sha384 -binary | openssl base64 -A +``` + +参考: + + +### SRI ハッシュ値の生成方法(外部サイトから読み込んでいるスクリプトについて) + +- 外部サイトから読み込んでいるスクリプトを更新した場合、下記のMDNオンラインツールでハッシュ値を生成する + - [SRI Hash Generator](https://www.srihash.org/) + +### SRI ハッシュ値の設定方法 + +- 更新したスクリプトを読み込んでいる箇所の`integrity`属性値を、生成したハッシュ値に置き換える +- 以下は設定のサンプル + +```bash + +``` diff --git a/ecs/jskult-webapp/src/controller/bio_api.py b/ecs/jskult-webapp/src/controller/bio_api.py index 982ef8d2..4989a50d 100644 --- a/ecs/jskult-webapp/src/controller/bio_api.py +++ b/ecs/jskult-webapp/src/controller/bio_api.py @@ -79,11 +79,11 @@ def search_bio_data( 'data': data, 'count': bio_sales_lot_count }) + # クッキーも書き換え json_response.set_cookie( key='session', value=session.session_key, - max_age=environment.SESSION_EXPIRE_MINUTE * 60, # cookieの有効期限は秒数指定なので、60秒をかける secure=True, httponly=True ) @@ -153,10 +153,10 @@ async def download_bio_data( 'status': 'ok', 'download_url': download_file_url }) + json_response.set_cookie( key='session', value=session.session_key, - max_age=environment.SESSION_EXPIRE_MINUTE * 60, # cookieの有効期限は秒数指定なので、60秒をかける secure=True, httponly=True ) diff --git a/ecs/jskult-webapp/src/controller/login.py b/ecs/jskult-webapp/src/controller/login.py index c8a5663c..5c8d904e 100644 --- a/ecs/jskult-webapp/src/controller/login.py +++ b/ecs/jskult-webapp/src/controller/login.py @@ -113,6 +113,7 @@ def login( status_code=status.HTTP_303_SEE_OTHER, headers={'session_key': session_key} ) + return response @@ -170,4 +171,5 @@ def sso_authorize( status_code=status.HTTP_303_SEE_OTHER, headers={'session_key': session_key} ) + return response diff --git a/ecs/jskult-webapp/src/controller/logout.py b/ecs/jskult-webapp/src/controller/logout.py index 8a633f6c..76d9ef60 100644 --- a/ecs/jskult-webapp/src/controller/logout.py +++ b/ecs/jskult-webapp/src/controller/logout.py @@ -1,50 +1,57 @@ -from typing import Optional, Union - -from fastapi import APIRouter, Depends, Request -from fastapi.responses import HTMLResponse - -from src.depends.auth import get_current_session -from src.model.internal.session import UserSession -from src.model.view.logout_view_model import LogoutViewModel -from src.system_var import constants -from src.templates import templates - -router = APIRouter() - -######################### -# Views # -######################### - - -@router.get('/', response_class=HTMLResponse) -def logout_view( - request: Request, - reason: Optional[str] = None, - session: Union[UserSession, None] = Depends(get_current_session) -): - # どういうルートでログインしたかを判断するため、refererを取得 - referer = request.headers.get('referer', '') - - redirect_to = '/login/userlogin' - link_text = 'MeDaCA機能メニューへ' - # セッションが切れておらず、メンテユーザである、またはメンテログイン画面から遷移した場合、メンテログイン画面に戻す - if (session is not None and session.user_flg == str(constants.PERMISSION_ENABLED)) \ - or referer.endswith('maintlogin'): - redirect_to = '/login/maintlogin' - link_text = 'Login画面に戻る' - - logout = LogoutViewModel( - redirect_to=redirect_to, - reason=constants.LOGOUT_REASON_MESSAGE_MAP.get(reason, ''), - link_text=link_text - ) - template_response = templates.TemplateResponse( - 'logout.html', - { - 'request': request, - 'logout': logout, - } - ) - # クッキーを削除 - template_response.delete_cookie('session') - return template_response +from typing import Optional, Union + +from fastapi import APIRouter, Depends, Request +from fastapi.responses import HTMLResponse + +from src.depends.auth import get_current_session +from src.model.internal.session import UserSession +from src.model.view.logout_view_model import LogoutViewModel +from src.system_var import constants +from src.templates import templates +from src.services import session_service + +router = APIRouter() + +######################### +# Views # +######################### + + + +@router.get('/', response_class=HTMLResponse) +def logout_view( + request: Request, + reason: Optional[str] = None, + session: Union[UserSession, None] = Depends(get_current_session) +): + # どういうルートでログインしたかを判断するため、refererを取得 + referer = request.headers.get('referer', '') + + redirect_to = '/login/userlogin' + link_text = 'MeDaCA機能メニューへ' + # セッションが切れておらず、メンテユーザである、またはメンテログイン画面から遷移した場合、メンテログイン画面に戻す + if (session is not None and session.user_flg == str(constants.PERMISSION_ENABLED)) \ + or referer.endswith('maintlogin'): + redirect_to = '/login/maintlogin' + link_text = 'Login画面に戻る' + + logout = LogoutViewModel( + redirect_to=redirect_to, + reason=constants.LOGOUT_REASON_MESSAGE_MAP.get(reason, ''), + link_text=link_text + ) + template_response = templates.TemplateResponse( + 'logout.html', + { + 'request': request, + 'logout': logout, + } + ) + # クッキーを削除 + template_response.delete_cookie('session') + + # セッション削除 + if session: + session_service.delete_session(session) + + return template_response diff --git a/ecs/jskult-webapp/src/main.py b/ecs/jskult-webapp/src/main.py index 174f097a..f0e1807d 100644 --- a/ecs/jskult-webapp/src/main.py +++ b/ecs/jskult-webapp/src/main.py @@ -10,8 +10,9 @@ from src.controller import (bio, bio_api, healthcheck, login, logout, from src.core import task from src.error.exception_handler import http_exception_handler from src.error.exceptions import UnexpectedException +from src.middleware.middleware import SecurityHeadersMiddleware -app = FastAPI() +app = FastAPI(openapi_url=None) # 静的ファイルをマウント app.mount('/static', StaticFiles(directory=path.dirname(static.__file__)), name='static') @@ -42,5 +43,8 @@ app.add_exception_handler(status.HTTP_403_FORBIDDEN, http_exception_handler) # サーバーエラーが発生した場合のハンドラー。HTTPExceptionではハンドリングできないため、個別に設定 app.add_exception_handler(UnexpectedException, http_exception_handler) +# セキュリティヘッダー設定はミドルウェアで処理する +app.add_middleware(SecurityHeadersMiddleware) + # サーバー起動時のイベント app.add_event_handler('startup', task.create_start_app_handler()) diff --git a/ecs/jskult-webapp/src/middleware/__init__.py b/ecs/jskult-webapp/src/middleware/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ecs/jskult-webapp/src/middleware/middleware.py b/ecs/jskult-webapp/src/middleware/middleware.py new file mode 100644 index 00000000..264fd190 --- /dev/null +++ b/ecs/jskult-webapp/src/middleware/middleware.py @@ -0,0 +1,16 @@ +from fastapi import Request, Response, status +from fastapi.responses import JSONResponse +from starlette.middleware.base import BaseHTTPMiddleware + +class SecurityHeadersMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request, call_next): + response = await call_next(request) + # X-Frame-Optionsヘッダー追加 + response.headers['X-Frame-Options'] = 'DENY' + # X-Content-Type-Optionsヘッダー追加 + response.headers['X-Content-Type-Options'] = 'nosniff' + # Strict-Transport-Securityヘッダー追加 + response.headers['Strict-Transport-Security'] = 'max-age=31536000 includeSubDomains' + # Cache-Controlヘッダー追加 + response.headers['Cache-Control'] = 'private' + return response diff --git a/ecs/jskult-webapp/src/router/session_router.py b/ecs/jskult-webapp/src/router/session_router.py index 9389a722..30690590 100644 --- a/ecs/jskult-webapp/src/router/session_router.py +++ b/ecs/jskult-webapp/src/router/session_router.py @@ -103,6 +103,7 @@ class AfterSetCookieSessionRoute(MeDaCaRoute): """事後処理として、セッションキーをcookieに設定するカスタムルートハンドラー""" async def post_process_route(self, request: Request, response: Response): response = await super().post_process_route(request, response) + session_key = response.headers.get('session_key', None) # セッションキーがない場合はセットせずに返す if session_key is None: @@ -123,7 +124,6 @@ class AfterSetCookieSessionRoute(MeDaCaRoute): response.set_cookie( key='session', value=session_key, - max_age=environment.SESSION_EXPIRE_MINUTE * 60, # cookieの有効期限は秒数指定なので、60秒をかける secure=True, httponly=True ) diff --git a/ecs/jskult-webapp/src/services/session_service.py b/ecs/jskult-webapp/src/services/session_service.py index 657e648d..b58d7241 100644 --- a/ecs/jskult-webapp/src/services/session_service.py +++ b/ecs/jskult-webapp/src/services/session_service.py @@ -1,19 +1,26 @@ - -from src.logging.get_logger import get_logger -from src.model.internal.session import UserSession - -logger = get_logger('セッション管理') - - -def set_session(session: UserSession) -> str: - session.save() - return session.session_key - - -def get_session(key: str) -> UserSession: - try: - session = UserSession.get(hash_key=key, consistent_read=True) - return session - except UserSession.DoesNotExist as e: - logger.debug(f'セッション取得失敗:{e}') - return None + +from src.logging.get_logger import get_logger +from src.model.internal.session import UserSession + +logger = get_logger('セッション管理') + + +def set_session(session: UserSession) -> str: + session.save() + return session.session_key + + +def get_session(key: str) -> UserSession: + try: + session = UserSession.get(hash_key=key, consistent_read=True) + return session + except UserSession.DoesNotExist as e: + logger.debug(f'セッション取得失敗:{e}') + return None + +def delete_session (session: UserSession): + try: + session.delete() + return + except: + return \ No newline at end of file diff --git a/ecs/jskult-webapp/src/templates/_header.html b/ecs/jskult-webapp/src/templates/_header.html index a0e90606..95ac2464 100644 --- a/ecs/jskult-webapp/src/templates/_header.html +++ b/ecs/jskult-webapp/src/templates/_header.html @@ -1,19 +1,19 @@ - + + {{subtitle}} - - - - - - - - - - - - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/ecs/jskult-webapp/src/templates/logout.html b/ecs/jskult-webapp/src/templates/logout.html index ed17c630..cc79a4ac 100644 --- a/ecs/jskult-webapp/src/templates/logout.html +++ b/ecs/jskult-webapp/src/templates/logout.html @@ -15,61 +15,8 @@ {{logout.reason}} {% endautoescape %}

-


{{logout.link_text}}

- - - - \ No newline at end of file