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..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; @@ -163,6 +172,32 @@ 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), 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 { + 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; + + 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..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; @@ -183,6 +192,32 @@ 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), 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 { + 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; + + 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..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; @@ -198,6 +207,32 @@ 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), 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 { + 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; + + 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;