From cf2cfa6c8cec5898dd89de5ec5b75543ae778667 Mon Sep 17 00:00:00 2001 From: Masaharu Hayashi Date: Mon, 28 Sep 2026 23:11:24 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix(weko-accounts):=20Shibboleth=20SP=20?= =?UTF-8?q?=E3=81=AE=E5=B1=9E=E6=80=A7=E3=81=AE=E5=8F=97=E3=81=91=E4=BB=98?= =?UTF-8?q?=E3=81=91=E5=85=83=E3=82=92=20SP=20=E3=81=AE=E3=83=AD=E3=82=B0?= =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=82=B9=E3=82=AF=E3=83=AA=E3=83=97=E3=83=88?= =?UTF-8?q?=E3=81=AB=E9=99=90=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SP の属性は、Web サーバ上のログインスクリプト(secure/login.py, login.php)が WEKO へ送る。その受け付けを、送信元のアドレスで限定する。 - weko-accounts: 受け付けるアドレスを WEKO_ACCOUNTS_SHIB_SP_ALLOWED_ADDRS (既定 127.0.0.1 / ::1)に限るデコレータを付ける。それ以外は 403 - login.py / login.php: 送り先をループバックアドレスにし、Host ヘッダに 公開ホスト名を入れる - nginx: /weko/shib/login への GET 以外をループバックアドレスからだけ許可する (weko.conf / weko-ams.conf / weko-ams-restricted.conf) 既存の環境では、login.py / login.php と nginx の設定を同時に更新すること。 どちらかだけでは Shibboleth ログインが通らなくなる。 Co-Authored-By: Claude Opus 5.5 --- modules/weko-accounts/tests/test_views.py | 32 +++++++++++++++++++ modules/weko-accounts/weko_accounts/config.py | 8 +++++ modules/weko-accounts/weko_accounts/utils.py | 21 ++++++++++++ modules/weko-accounts/weko_accounts/views.py | 4 ++- nginx/login.php | 6 +++- nginx/login.py | 10 ++++-- nginx/weko-ams-restricted.conf | 24 ++++++++++++++ nginx/weko-ams.conf | 24 ++++++++++++++ nginx/weko.conf | 24 ++++++++++++++ 9 files changed, 149 insertions(+), 4 deletions(-) diff --git a/modules/weko-accounts/tests/test_views.py b/modules/weko-accounts/tests/test_views.py index c6f29a36f8..215d2b2d50 100644 --- a/modules/weko-accounts/tests/test_views.py +++ b/modules/weko-accounts/tests/test_views.py @@ -783,6 +783,38 @@ def test_shib_login(client,redis_connect,users,mocker): assert "Server error has occurred. Please contact server " \ "administrator." in called_kwargs.get("ams_error", "") +# .tox/c1/bin/pytest --cov=weko_accounts tests/test_views.py::test_shib_sp_login_source -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-accounts/.tox/c1/tmp +def test_shib_sp_login_source(app, client, mocker): + """The attributes of the SP are accepted only from the allowed addresses.""" + url = url_for("weko_accounts.shib_sp_login") + mock_parse = mocker.patch("weko_accounts.views.parse_attributes", + return_value=({}, True)) + data = {"Shib-Session-ID": "dummy"} + + # other addresses are rejected before the attributes are read + res = client.post(url, data=data, + environ_base={"REMOTE_ADDR": "203.0.113.10"}) + assert res.status_code == 403 + mock_parse.assert_not_called() + + # the loopback address is accepted (the default of the test client) + client.post(url, data=data, environ_base={"REMOTE_ADDR": "127.0.0.1"}) + assert mock_parse.called + + # the allowed addresses follow the configuration + mock_parse.reset_mock() + app.config["WEKO_ACCOUNTS_SHIB_SP_ALLOWED_ADDRS"] = ["203.0.113.10"] + try: + client.post(url, data=data, + environ_base={"REMOTE_ADDR": "203.0.113.10"}) + assert mock_parse.called + res = client.post(url, data=data, + environ_base={"REMOTE_ADDR": "127.0.0.1"}) + assert res.status_code == 403 + finally: + app.config["WEKO_ACCOUNTS_SHIB_SP_ALLOWED_ADDRS"] = ["127.0.0.1", "::1"] + + #def shib_sp_login(): # .tox/c1/bin/pytest --cov=weko_accounts tests/test_views.py::test_shib_sp_login -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-workflow/.tox/c1/tmp def test_shib_sp_login(client, redis_connect,mocker, db, users): diff --git a/modules/weko-accounts/weko_accounts/config.py b/modules/weko-accounts/weko_accounts/config.py index a7c7a9eeba..5c6a9de7c9 100644 --- a/modules/weko-accounts/weko_accounts/config.py +++ b/modules/weko-accounts/weko_accounts/config.py @@ -29,6 +29,14 @@ WEKO_ACCOUNTS_SHIB_LOGIN_ENABLED = False """Enable Shibboleth user login system.""" +WEKO_ACCOUNTS_SHIB_SP_ALLOWED_ADDRS = ['127.0.0.1', '::1'] +"""Addresses allowed to post the attributes of the Shibboleth SP. + +The attributes are posted by the login script of the SP (nginx/login.py), +which runs on the web server and posts to the loopback address. The address +is the one nginx passes as REMOTE_ADDR. +""" + WEKO_ACCOUNTS_SHIB_CACHE_PREFIX = 'Shib-Session-' """Shibboleth cache prefix info.""" diff --git a/modules/weko-accounts/weko_accounts/utils.py b/modules/weko-accounts/weko_accounts/utils.py index eed4ea6017..7e9e8e97e5 100644 --- a/modules/weko-accounts/weko_accounts/utils.py +++ b/modules/weko-accounts/weko_accounts/utils.py @@ -111,6 +111,27 @@ def generate_random_str(length=128): ) +def shib_sp_source_required(f): + """Accept the request only from the addresses of the Shibboleth SP. + + The attributes of the IdP are taken from the request itself, so they must + come only from the login script of the SP. The address is checked against + ``WEKO_ACCOUNTS_SHIB_SP_ALLOWED_ADDRS``, and any other request is rejected + with 403. + """ + @wraps(f) + def decorated(*args, **kwargs): + allowed = current_app.config.get( + 'WEKO_ACCOUNTS_SHIB_SP_ALLOWED_ADDRS', []) + if request.remote_addr not in allowed: + current_app.logger.warning( + 'Shibboleth SP attributes from a disallowed address: {}'.format( + request.remote_addr)) + abort(403) + return f(*args, **kwargs) + return decorated + + def parse_attributes(): """Parse arguments from environment variables.""" attrs = {} diff --git a/modules/weko-accounts/weko_accounts/views.py b/modules/weko-accounts/weko_accounts/views.py index 23247701fa..2ca903cef2 100644 --- a/modules/weko-accounts/weko_accounts/views.py +++ b/modules/weko-accounts/weko_accounts/views.py @@ -44,7 +44,8 @@ from weko_logging.activity_logger import UserActivityLogger from .api import ShibUser, sync_shib_gakunin_map_groups -from .utils import generate_random_str, parse_attributes +from .utils import generate_random_str, parse_attributes, \ + shib_sp_source_required _app = LocalProxy(lambda: current_app.extensions['weko-admin'].app) @@ -544,6 +545,7 @@ def find_user_by_email(shib_attributes): return user @blueprint.route('/shib/login', methods=['POST']) +@shib_sp_source_required def shib_sp_login(): """The request from shibboleth sp. diff --git a/nginx/login.php b/nginx/login.php index feaadc549a..3aec5844a1 100644 --- a/nginx/login.php +++ b/nginx/login.php @@ -15,7 +15,10 @@ }else{ $next='%2F'; } - $url = $base."/weko/shib/login?next=".$next; + // Post to the loopback address. WEKO accepts the attributes only from the + // addresses in WEKO_ACCOUNTS_SHIB_SP_ALLOWED_ADDRS, and nginx allows the + // POST to /weko/shib/login only from the loopback address. + $url = $_SERVER['REQUEST_SCHEME']."://127.0.0.1/weko/shib/login?next=".$next; $curl = curl_init(); $post_args=[]; $post_args['SHIB_ATTR_USER_NAME']=$_SERVER['HTTP_WEKOID']; @@ -32,6 +35,7 @@ $cookie=tempnam(sys_get_temp_dir(),'cookie_'); //set options curl_setopt($curl,CURLOPT_URL,$url); + curl_setopt($curl, CURLOPT_HTTPHEADER, array('Host: '.$_SERVER['SERVER_NAME'])); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl,CURLOPT_SSL_VERIFYPEER, false); curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0); diff --git a/nginx/login.py b/nginx/login.py index 558ca83c28..35470fa6c1 100644 --- a/nginx/login.py +++ b/nginx/login.py @@ -22,7 +22,12 @@ next = qs['next'][0] else: next = '%2F' - url = base_url + '/weko/shib/login?next=' + next + # Post to the loopback address. WEKO accepts the attributes only from the + # addresses in WEKO_ACCOUNTS_SHIB_SP_ALLOWED_ADDRS, and nginx allows the + # POST to /weko/shib/login only from the loopback address. + url = os.environ.get('REQUEST_SCHEME') + '://127.0.0.1' \ + + '/weko/shib/login?next=' + next + headers = {'Host': os.environ.get('HTTP_HOST')} # Get the fastcgi_params fastcgi_params = [] @@ -47,7 +52,8 @@ cookie_jar = cookiejar.LWPCookieJar(temp_path) # Request to the Shibboleth login - response = requests.post(url, data=data, verify=False, cookies=cookie_jar) + response = requests.post(url, data=data, headers=headers, verify=False, + cookies=cookie_jar) response.raise_for_status() redirect = response.text diff --git a/nginx/weko-ams-restricted.conf b/nginx/weko-ams-restricted.conf index 9d97dc33a2..d586c7a9ce 100644 --- a/nginx/weko-ams-restricted.conf +++ b/nginx/weko-ams-restricted.conf @@ -163,6 +163,30 @@ server { # proxy_set_header X-Forwarded-Proto $scheme; # } + # The attributes of the Shibboleth SP are posted only by the login script + # on this server (secure/login.py, login.php), to the loopback address. + # WEKO also checks the address (WEKO_ACCOUNTS_SHIB_SP_ALLOWED_ADDRS). + location = /weko/shib/login { + limit_except GET { + allow 127.0.0.1; + allow ::1; + deny all; + } + uwsgi_pass app_server; + include uwsgi_params; + + uwsgi_buffering off; + uwsgi_request_buffering off; + + uwsgi_buffer_size 32k; + uwsgi_buffers 8 32k; + uwsgi_busy_buffers_size 32k; + + uwsgi_param Host $host; + uwsgi_param X-Forwarded-For $proxy_add_x_forwarded_for; + uwsgi_param X-Forwarded-Proto $scheme; + } + location /weko/shib { uwsgi_pass app_server; include uwsgi_params; diff --git a/nginx/weko-ams.conf b/nginx/weko-ams.conf index c5b37d07f8..fd8f25cb50 100644 --- a/nginx/weko-ams.conf +++ b/nginx/weko-ams.conf @@ -183,6 +183,30 @@ server { # proxy_set_header X-Forwarded-Proto $scheme; # } + # The attributes of the Shibboleth SP are posted only by the login script + # on this server (secure/login.py, login.php), to the loopback address. + # WEKO also checks the address (WEKO_ACCOUNTS_SHIB_SP_ALLOWED_ADDRS). + location = /weko/shib/login { + limit_except GET { + allow 127.0.0.1; + allow ::1; + deny all; + } + uwsgi_pass app_server; + include uwsgi_params; + + uwsgi_buffering off; + uwsgi_request_buffering off; + + uwsgi_buffer_size 32k; + uwsgi_buffers 8 32k; + uwsgi_busy_buffers_size 32k; + + uwsgi_param Host $host; + uwsgi_param X-Forwarded-For $proxy_add_x_forwarded_for; + uwsgi_param X-Forwarded-Proto $scheme; + } + location /weko/shib { uwsgi_pass app_server; include uwsgi_params; diff --git a/nginx/weko.conf b/nginx/weko.conf index 907134dcf2..f9b8a3963e 100644 --- a/nginx/weko.conf +++ b/nginx/weko.conf @@ -198,6 +198,30 @@ server { # proxy_set_header X-Forwarded-Proto $scheme; # } + # The attributes of the Shibboleth SP are posted only by the login script + # on this server (secure/login.py, login.php), to the loopback address. + # WEKO also checks the address (WEKO_ACCOUNTS_SHIB_SP_ALLOWED_ADDRS). + location = /weko/shib/login { + limit_except GET { + allow 127.0.0.1; + allow ::1; + deny all; + } + uwsgi_pass app_server; + include uwsgi_params; + + uwsgi_buffering off; + uwsgi_request_buffering off; + + uwsgi_buffer_size 32k; + uwsgi_buffers 8 32k; + uwsgi_busy_buffers_size 32k; + + uwsgi_param Host $host; + uwsgi_param X-Forwarded-For $proxy_add_x_forwarded_for; + uwsgi_param X-Forwarded-Proto $scheme; + } + location /weko/shib { uwsgi_pass app_server; include uwsgi_params; From 8c2ab33c18640ab1c37a8930fbe6118592159866 Mon Sep 17 00:00:00 2001 From: Masaharu Hayashi Date: Mon, 28 Sep 2026 23:29:35 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix(nginx):=20SP=20=E3=81=AE=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=E3=81=AE=E5=8F=97=E3=81=91=E4=BB=98=E3=81=91=E5=88=A4?= =?UTF-8?q?=E5=AE=9A=E3=82=92=E3=80=81real=5Fip=20=E3=81=A7=E6=9B=B8?= =?UTF-8?q?=E3=81=8D=E6=8F=9B=E3=81=88=E3=82=8B=E5=89=8D=E3=81=AE=E6=8E=A5?= =?UTF-8?q?=E7=B6=9A=E5=85=83=E3=81=A7=E8=A1=8C=E3=81=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit real_ip は信頼するプロキシ(プライベートアドレス)からの接続について X-Forwarded-For で $remote_addr を書き換える。そのため $remote_addr で ループバックかを判定すると、信頼するアドレスから X-Forwarded-For に ループバックを入れた要求を通してしまう。 /weko/shib/login の判定を $realip_remote_addr(書き換える前の接続元)で 行い、WEKO へ渡す REMOTE_ADDR も同じ値にする。 Co-Authored-By: Claude Opus 5.5 --- nginx/weko-ams-restricted.conf | 23 +++++++++++++++++------ nginx/weko-ams.conf | 23 +++++++++++++++++------ nginx/weko.conf | 23 +++++++++++++++++------ 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/nginx/weko-ams-restricted.conf b/nginx/weko-ams-restricted.conf index d586c7a9ce..e1ba9c264a 100644 --- a/nginx/weko-ams-restricted.conf +++ b/nginx/weko-ams-restricted.conf @@ -14,6 +14,15 @@ map $request_uri $new { include /etc/nginx/redirect_list.map; } +# POST to /weko/shib/login is allowed only from the loopback address of the +# connection (see location = /weko/shib/login). +map "$request_method:$realip_remote_addr" $weko_shib_sp_denied { + default 1; + "~^(GET|HEAD):" 0; + "POST:127.0.0.1" 0; + "POST:::1" 0; +} + server { listen 80; return 301 https://$host$request_uri; @@ -164,16 +173,18 @@ server { # } # The attributes of the Shibboleth SP are posted only by the login script - # on this server (secure/login.py, login.php), to the loopback address. - # WEKO also checks the address (WEKO_ACCOUNTS_SHIB_SP_ALLOWED_ADDRS). + # on this server (secure/login.py, login.php), over the loopback address. + # The address is the one of the connection, before real_ip rewrites it with + # X-Forwarded-For, so that a trusted proxy address cannot be used to pose + # as the loopback address. WEKO checks the same address + # (WEKO_ACCOUNTS_SHIB_SP_ALLOWED_ADDRS). location = /weko/shib/login { - limit_except GET { - allow 127.0.0.1; - allow ::1; - deny all; + if ($weko_shib_sp_denied) { + return 403; } uwsgi_pass app_server; include uwsgi_params; + uwsgi_param REMOTE_ADDR $realip_remote_addr; uwsgi_buffering off; uwsgi_request_buffering off; diff --git a/nginx/weko-ams.conf b/nginx/weko-ams.conf index fd8f25cb50..22c7e86122 100644 --- a/nginx/weko-ams.conf +++ b/nginx/weko-ams.conf @@ -14,6 +14,15 @@ map $request_uri $new { include /etc/nginx/redirect_list.map; } +# POST to /weko/shib/login is allowed only from the loopback address of the +# connection (see location = /weko/shib/login). +map "$request_method:$realip_remote_addr" $weko_shib_sp_denied { + default 1; + "~^(GET|HEAD):" 0; + "POST:127.0.0.1" 0; + "POST:::1" 0; +} + server { listen 80; return 301 https://$host$request_uri; @@ -184,16 +193,18 @@ server { # } # The attributes of the Shibboleth SP are posted only by the login script - # on this server (secure/login.py, login.php), to the loopback address. - # WEKO also checks the address (WEKO_ACCOUNTS_SHIB_SP_ALLOWED_ADDRS). + # on this server (secure/login.py, login.php), over the loopback address. + # The address is the one of the connection, before real_ip rewrites it with + # X-Forwarded-For, so that a trusted proxy address cannot be used to pose + # as the loopback address. WEKO checks the same address + # (WEKO_ACCOUNTS_SHIB_SP_ALLOWED_ADDRS). location = /weko/shib/login { - limit_except GET { - allow 127.0.0.1; - allow ::1; - deny all; + if ($weko_shib_sp_denied) { + return 403; } uwsgi_pass app_server; include uwsgi_params; + uwsgi_param REMOTE_ADDR $realip_remote_addr; uwsgi_buffering off; uwsgi_request_buffering off; diff --git a/nginx/weko.conf b/nginx/weko.conf index f9b8a3963e..9891084820 100644 --- a/nginx/weko.conf +++ b/nginx/weko.conf @@ -11,6 +11,15 @@ map $request_uri $new { include /etc/nginx/redirect_list.map; } +# POST to /weko/shib/login is allowed only from the loopback address of the +# connection (see location = /weko/shib/login). +map "$request_method:$realip_remote_addr" $weko_shib_sp_denied { + default 1; + "~^(GET|HEAD):" 0; + "POST:127.0.0.1" 0; + "POST:::1" 0; +} + server { listen 80; return 301 https://$host$request_uri; @@ -199,16 +208,18 @@ server { # } # The attributes of the Shibboleth SP are posted only by the login script - # on this server (secure/login.py, login.php), to the loopback address. - # WEKO also checks the address (WEKO_ACCOUNTS_SHIB_SP_ALLOWED_ADDRS). + # on this server (secure/login.py, login.php), over the loopback address. + # The address is the one of the connection, before real_ip rewrites it with + # X-Forwarded-For, so that a trusted proxy address cannot be used to pose + # as the loopback address. WEKO checks the same address + # (WEKO_ACCOUNTS_SHIB_SP_ALLOWED_ADDRS). location = /weko/shib/login { - limit_except GET { - allow 127.0.0.1; - allow ::1; - deny all; + if ($weko_shib_sp_denied) { + return 403; } uwsgi_pass app_server; include uwsgi_params; + uwsgi_param REMOTE_ADDR $realip_remote_addr; uwsgi_buffering off; uwsgi_request_buffering off;