Merge remote-tracking branch 'origin/develop-fix-webapp-vulnerability' into release-202403

This commit is contained in:
shimoda.m@nds-tyo.co.jp 2024-02-27 13:37:31 +09:00
commit 42d3d80199
11 changed files with 162 additions and 141 deletions

View File

@ -84,6 +84,8 @@
│   ├── exception_handler.py -- FastAPI内部でエラー発生時のハンドリング │   ├── exception_handler.py -- FastAPI内部でエラー発生時のハンドリング
│   └── exceptions.py -- カスタム例外クラス │   └── exceptions.py -- カスタム例外クラス
├── main.py -- APサーバーのエントリーポイント。ここでルーターやハンドラーの登録を行う ├── main.py -- APサーバーのエントリーポイント。ここでルーターやハンドラーの登録を行う
├── middleware -- ミドルウェアの設定
│ └── middleware.py
├── model -- モデル層(MVCのM) ├── model -- モデル層(MVCのM)
│   ├── db -- リポジトリから返されるDBレコードのモデル │   ├── db -- リポジトリから返されるDBレコードのモデル
│   │   ├── base_db_model.py │   │   ├── base_db_model.py
@ -195,3 +197,39 @@
- コントローラーのrouter変数が、`router.route_class = Authenticate`となっている場合、以下の動きをする - コントローラーのrouter変数が、`router.route_class = Authenticate`となっている場合、以下の動きをする
- リクエスト到達時にセッションの有無をチェックする - リクエスト到達時にセッションの有無をチェックする
- レスポンス時、クッキーにセッションキーを登録する - レスポンス時、クッキーにセッションキーを登録する
## HTMLで読み込んでいるスクリプトのSRIハッシュ値を生成・設定する方法
### サブリソース完全性 (Subresource Integrity, SRI) とは
CDN などから取得したリソースが意図せず改ざんされていないかをブラウザーが検証するセキュリティ機能です。 SRI を利用する際には、取得したリソースのハッシュ値と一致すべきハッシュ値を指定します。
詳細:<https://developer.mozilla.org/ja/docs/Web/Security/Subresource_Integrity>
実消化&アルトマークのWebアプリケーションでは、複数の外部スクリプトを読み込んで動作しているため、読み込むスクリプトを変更した場合は、
タグの属性値`integrity`に設定されているスクリプトのハッシュ値を更新する必要がある。
### SRI ハッシュ値の生成方法(サーバー内のスクリプトについて)
- サーバー内に保管されているスクリプトを更新した場合、Linux環境WSL2でも可で、以下のコマンドを実行し、ハッシュ値を生成する
```bash
cat <更新したスクリプトファイル名> | openssl dgst -sha384 -binary | openssl base64 -A
```
参考:<https://developer.mozilla.org/ja/docs/Web/Security/Subresource_Integrity#sri_%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E3%82%92%E7%94%9F%E6%88%90%E3%81%99%E3%82%8B%E3%83%84%E3%83%BC%E3%83%AB>
### SRI ハッシュ値の生成方法(外部サイトから読み込んでいるスクリプトについて)
- 外部サイトから読み込んでいるスクリプトを更新した場合、下記のMDNオンラインツールでハッシュ値を生成する
- [SRI Hash Generator](https://www.srihash.org/)
### SRI ハッシュ値の設定方法
- 更新したスクリプトを読み込んでいる箇所の`integrity`属性値を、生成したハッシュ値に置き換える
- 以下は設定のサンプル
```bash
<script src="https://リンク/スクリプト.js" integrity="sha384-生成したハッシュ" crossorigin="anonymous"></script>
```

View File

@ -79,11 +79,11 @@ def search_bio_data(
'data': data, 'data': data,
'count': bio_sales_lot_count 'count': bio_sales_lot_count
}) })
# クッキーも書き換え # クッキーも書き換え
json_response.set_cookie( json_response.set_cookie(
key='session', key='session',
value=session.session_key, value=session.session_key,
max_age=environment.SESSION_EXPIRE_MINUTE * 60, # cookieの有効期限は秒数指定なので、60秒をかける
secure=True, secure=True,
httponly=True httponly=True
) )
@ -153,10 +153,10 @@ async def download_bio_data(
'status': 'ok', 'status': 'ok',
'download_url': download_file_url 'download_url': download_file_url
}) })
json_response.set_cookie( json_response.set_cookie(
key='session', key='session',
value=session.session_key, value=session.session_key,
max_age=environment.SESSION_EXPIRE_MINUTE * 60, # cookieの有効期限は秒数指定なので、60秒をかける
secure=True, secure=True,
httponly=True httponly=True
) )

View File

@ -113,6 +113,7 @@ def login(
status_code=status.HTTP_303_SEE_OTHER, status_code=status.HTTP_303_SEE_OTHER,
headers={'session_key': session_key} headers={'session_key': session_key}
) )
return response return response
@ -170,4 +171,5 @@ def sso_authorize(
status_code=status.HTTP_303_SEE_OTHER, status_code=status.HTTP_303_SEE_OTHER,
headers={'session_key': session_key} headers={'session_key': session_key}
) )
return response return response

View File

@ -1,50 +1,57 @@
from typing import Optional, Union from typing import Optional, Union
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from src.depends.auth import get_current_session from src.depends.auth import get_current_session
from src.model.internal.session import UserSession from src.model.internal.session import UserSession
from src.model.view.logout_view_model import LogoutViewModel from src.model.view.logout_view_model import LogoutViewModel
from src.system_var import constants from src.system_var import constants
from src.templates import templates from src.templates import templates
from src.services import session_service
router = APIRouter()
router = APIRouter()
#########################
# Views # #########################
######################### # Views #
#########################
@router.get('/', response_class=HTMLResponse)
def logout_view(
request: Request, @router.get('/', response_class=HTMLResponse)
reason: Optional[str] = None, def logout_view(
session: Union[UserSession, None] = Depends(get_current_session) request: Request,
): reason: Optional[str] = None,
# どういうルートでログインしたかを判断するため、refererを取得 session: Union[UserSession, None] = Depends(get_current_session)
referer = request.headers.get('referer', '') ):
# どういうルートでログインしたかを判断するため、refererを取得
redirect_to = '/login/userlogin' referer = request.headers.get('referer', '')
link_text = 'MeDaCA機能メニューへ'
# セッションが切れておらず、メンテユーザである、またはメンテログイン画面から遷移した場合、メンテログイン画面に戻す redirect_to = '/login/userlogin'
if (session is not None and session.user_flg == str(constants.PERMISSION_ENABLED)) \ link_text = 'MeDaCA機能メニューへ'
or referer.endswith('maintlogin'): # セッションが切れておらず、メンテユーザである、またはメンテログイン画面から遷移した場合、メンテログイン画面に戻す
redirect_to = '/login/maintlogin' if (session is not None and session.user_flg == str(constants.PERMISSION_ENABLED)) \
link_text = 'Login画面に戻る' or referer.endswith('maintlogin'):
redirect_to = '/login/maintlogin'
logout = LogoutViewModel( link_text = 'Login画面に戻る'
redirect_to=redirect_to,
reason=constants.LOGOUT_REASON_MESSAGE_MAP.get(reason, ''), logout = LogoutViewModel(
link_text=link_text redirect_to=redirect_to,
) reason=constants.LOGOUT_REASON_MESSAGE_MAP.get(reason, ''),
template_response = templates.TemplateResponse( link_text=link_text
'logout.html', )
{ template_response = templates.TemplateResponse(
'request': request, 'logout.html',
'logout': logout, {
} 'request': request,
) 'logout': logout,
# クッキーを削除 }
template_response.delete_cookie('session') )
return template_response # クッキーを削除
template_response.delete_cookie('session')
# セッション削除
if session:
session_service.delete_session(session)
return template_response

View File

@ -10,8 +10,9 @@ from src.controller import (bio, bio_api, healthcheck, login, logout,
from src.core import task from src.core import task
from src.error.exception_handler import http_exception_handler from src.error.exception_handler import http_exception_handler
from src.error.exceptions import UnexpectedException 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') 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ではハンドリングできないため、個別に設定 # サーバーエラーが発生した場合のハンドラー。HTTPExceptionではハンドリングできないため、個別に設定
app.add_exception_handler(UnexpectedException, http_exception_handler) app.add_exception_handler(UnexpectedException, http_exception_handler)
# セキュリティヘッダー設定はミドルウェアで処理する
app.add_middleware(SecurityHeadersMiddleware)
# サーバー起動時のイベント # サーバー起動時のイベント
app.add_event_handler('startup', task.create_start_app_handler()) app.add_event_handler('startup', task.create_start_app_handler())

View File

@ -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

View File

@ -103,6 +103,7 @@ class AfterSetCookieSessionRoute(MeDaCaRoute):
"""事後処理として、セッションキーをcookieに設定するカスタムルートハンドラー""" """事後処理として、セッションキーをcookieに設定するカスタムルートハンドラー"""
async def post_process_route(self, request: Request, response: Response): async def post_process_route(self, request: Request, response: Response):
response = await super().post_process_route(request, response) response = await super().post_process_route(request, response)
session_key = response.headers.get('session_key', None) session_key = response.headers.get('session_key', None)
# セッションキーがない場合はセットせずに返す # セッションキーがない場合はセットせずに返す
if session_key is None: if session_key is None:
@ -123,7 +124,6 @@ class AfterSetCookieSessionRoute(MeDaCaRoute):
response.set_cookie( response.set_cookie(
key='session', key='session',
value=session_key, value=session_key,
max_age=environment.SESSION_EXPIRE_MINUTE * 60, # cookieの有効期限は秒数指定なので、60秒をかける
secure=True, secure=True,
httponly=True httponly=True
) )

View File

@ -1,19 +1,26 @@
from src.logging.get_logger import get_logger from src.logging.get_logger import get_logger
from src.model.internal.session import UserSession from src.model.internal.session import UserSession
logger = get_logger('セッション管理') logger = get_logger('セッション管理')
def set_session(session: UserSession) -> str: def set_session(session: UserSession) -> str:
session.save() session.save()
return session.session_key return session.session_key
def get_session(key: str) -> UserSession: def get_session(key: str) -> UserSession:
try: try:
session = UserSession.get(hash_key=key, consistent_read=True) session = UserSession.get(hash_key=key, consistent_read=True)
return session return session
except UserSession.DoesNotExist as e: except UserSession.DoesNotExist as e:
logger.debug(f'セッション取得失敗:{e}') logger.debug(f'セッション取得失敗:{e}')
return None return None
def delete_session (session: UserSession):
try:
session.delete()
return
except:
return

View File

@ -1,19 +1,19 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, address=no" http-equiv="content-type" content="text/html; charset=utf-8" /> <meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta name="format-detection" content="telephone=no, address=no" />
<title>{{subtitle}}</title> <title>{{subtitle}}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.2/font/bootstrap-icons.css" integrity="sha384-b6lVK+yci+bfDmaY1u0zE8YYJt0TZxLEAFyYSLHId4xoVvsrQu3INevFKo+Xir8e" crossorigin="anonymous"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css"> <link rel="stylesheet" href="/static/css/main_theme.css" integrity="sha384-k0YpJBvcGJXdJlt8yqnhPYuU7tHQfdv4C80KDdGf72dzzAVWUp+ek+A1cqOV5o4t">
<link rel="stylesheet" href="/static/css/main_theme.css"> <link rel="stylesheet" href="/static/css/pagenation.css" integrity="sha384-CDhOHftwvzWdI3cmvl0PESIdU5i0qjWbz8+HE9poJscglyrB0jzXZpVkb51xigty">
<link rel="stylesheet" href="/static/css/pagenation.css"> <link rel="stylesheet" href="/static/css/datepicker.css" integrity="sha384-I3gPqeqj0wDLoF6oS/OuMJ5C+BI210zLrJvQvNRVdvyyI9+qrraaQK2L9vvhTA8x">
<link rel="stylesheet" href="/static/css/datepicker.css"> <link rel="stylesheet" href="/static/css/loading.css" integrity="sha384-f9FRohCbLarb6Z91FWRbfNIIYYLx/5Kxqw19CB9Z0GxXunS9j0gRWWl50LayDAG7">
<link rel="stylesheet" href="/static/css/loading.css"> <script src="https://cdn.jsdelivr.net/npm/jquery@3.6.3/dist/jquery.min.js" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.3.min.js" integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/paginationjs@2.5.0/dist/pagination.min.js" crossorigin="anonymous"></script>
<script src="https://pagination.js.org/dist/2.5.0/pagination.min.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/ja.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/ja.min.js"></script> <script src="/static/function/businessLogicScript.js" integrity="sha384-ytd1o7Rx4BPzjO3RpzR9fW/Z4avGzS7+BRPZVUsQp5X4zXB6xdZpR47/En1mNl7s" crossorigin="anonymous"></script>
<script src="/static/function/businessLogicScript.js"></script> <script src="/static/lib/fixed_midashi.js" integrity="sha384-mCd6L3DNaLgUWyH051BywJfzlVavCkK6F0wbMqG+j7jAq174Uf7HJdq3H4wxCJKs" crossorigin="anonymous"></script>
<script src="/static/lib/fixed_midashi.js"></script>

View File

@ -15,61 +15,8 @@
{{logout.reason}} {{logout.reason}}
{% endautoescape %} {% endautoescape %}
</p> </p>
<!-- <?php
// getが来ておらず理由がわからない場合
if(!(isset($_GET['reason']))){
$userflg = null;
// ログアウトボタンを押されたとき
} else if($_GET['reason'] == 'logoutBtn'){
?>
<p class="logout_p"><?php echo $logoutMsg ?></p>
<?php
// ログイン失敗時に表示
} else if($_GET['reason'] == 'loginErr'){
?>
<p class="logout_p"><?php echo $loginErrMsg ?></p>
<?php
// 日時バッチ処理中エラー時
} else if($_GET['reason'] == 'batchProcess'){
?>
<p class="logout_p"><?php echo $batchProcessMsg ?></p>
<?php
// マスターメンテ日時バッチ処理中エラー時
} else if($_GET['reason'] == 'batchProcessNewInstEmpRegist'){
?>
<p class="logout_p"><?php echo $batchProcessNewInstEmpRegistMsg ?></p>
<?php
// どっちのユーザーでログインしたかわからないとき
} else if (!(isset($userflg))) {
} else {
$userflg = null;
?>
<p class="logout_p"><?php echo $unexpectedErrMsg ?></p>
<?php
}
?> -->
<br><br><br> <br><br><br>
<p class="logout_p"><a href="{{ logout.redirect_to }}">{{logout.link_text}}</a></p> <p class="logout_p"><a href="{{ logout.redirect_to }}">{{logout.link_text}}</a></p>
<!-- MeDaCA機能メニューへ -->
<!-- <p class="logout_p"><a href="redirect_to">Login画面に戻る</a></p> -->
<!-- <?php
if (!(isset($userflg))) {
?>
<p class="logout_p"><a href="<?php echo $groupwarePath ?>"><?php echo $groupwareBackMsg ?></a></p>
<?php
} else if($userflg == 1){
?>
<p class="logout_p"><a href="<?php echo $maintLoginPath ?>"><?php echo $loginBackMsg ?></a></p>
<?php
} else {
?>
<p class="logout_p"><a href="<?php echo $groupwarePath ?>"><?php echo $groupwareBackMsg ?></a></p>
<?php
}
?> -->
</div> </div>
</body> </body>
</html> </html>