Index: openacs-4/packages/acs-tcl/tcl/security-procs.tcl
===================================================================
RCS file: /usr/local/cvsroot/openacs-4/packages/acs-tcl/tcl/security-procs.tcl,v
diff -u -r1.126 -r1.127
--- openacs-4/packages/acs-tcl/tcl/security-procs.tcl 11 Feb 2019 11:52:48 -0000 1.126
+++ openacs-4/packages/acs-tcl/tcl/security-procs.tcl 3 Sep 2024 15:37:34 -0000 1.127
@@ -1,7 +1,7 @@
ad_library {
Provides methods for authorizing and identifying ACS users
- (both logged in and not) and tracking their sessions.
+ (both logged-in and not) and tracking their sessions.
@creation-date 16 Feb 2000
@author Jon Salz (jsalz@arsdigita.com)
@@ -31,14 +31,15 @@
# Cookies (all are signed cookies):
# cookie value max-age secure
# --------------------------------------------------------------------------------------------
-# ad_session_id session_id,user_id,login_level SessionTimeout yes|no
-# ad_user_login user_id,issue_time,auth_token,forever LoginTimeout|inf no
-# ad_user_login_secure user_id,issue_time,auth_token,random,forever LoginTimeout|inf yes
-# ad_secure_token session_id,random,peeraddr SessionLifetime yes
+# ad_session_id session_id,user_id,login_level SessionTimeout yes|no
+# ad_user_login user_id,issue_time,auth_token,forever,er LoginTimeout|inf no
+# ad_user_login_secure user_id,issue_time,auth_token,random,forever,er LoginTimeout|inf yes
+# ad_secure_token session_id,random,peeraddr SessionLifetime yes
#
# "random" is used to hinder attack the secure hash. Currently the
# random data is ns_time. "peeraddr" is used to avoid session
-# hijacking.
+# hijacking. "er" stands for external_registry and is only
+# nonempty, when an external registry is used.
#
# ad_user_login/ad_user_login_secure issue_time:
# [ns_time] at the time the user last authenticated
@@ -48,7 +49,7 @@
# 1 = ok,
# 2 = auth ok, but account closed
-ad_proc -private sec_random_token {} {
+ad_proc -public sec_random_token {} {
Generates a random token.
} {
# ::tcl_sec_seed is used to maintain a small subset of the previously
@@ -63,12 +64,13 @@
set request "yoursponsoredadvertisementhere"
set start_clicks "cvs.openacs.org"
}
-
- if { ![info exists ::tcl_sec_seed] } {
- set ::tcl_sec_seed "listentowmbr89.1"
+ if {[acs::icanuse "ns_crypto::randombytes"]} {
+ if {![info exists ::tcl_sec_seed]} { set ::tcl_sec_seed [ns_crypto::randombytes 16].$start_clicks }
+ set random_base [ns_sha1 "[ns_time][ns_crypto::randombytes -encoding binary 16]$start_clicks$request$::tcl_sec_seed"]
+ } else {
+ if {![info exists ::tcl_sec_seed]} { set ::tcl_sec_seed [ns_rand].$start_clicks }
+ set random_base [ns_sha1 "[ns_time][ns_rand]$start_clicks$request$::tcl_sec_seed"]
}
-
- set random_base [ns_sha1 "[ns_time][ns_rand]$start_clicks$request$::tcl_sec_seed"]
set ::tcl_sec_seed [string range $random_base 0 10]
return [ns_sha1 [string range $random_base 11 39]]
@@ -78,7 +80,10 @@
Returns the maximum lifetime, in seconds, for sessions.
} {
# default value is 7 days ( 7 * 24 * 60 * 60 )
- return [parameter::get -package_id [ad_acs_kernel_id] -parameter SessionLifetime -default 604800]
+ return [parameter::get \
+ -package_id $::acs::kernel_id \
+ -parameter SessionLifetime \
+ -default 604800]
}
ad_proc -private sec_sweep_sessions {} {
@@ -107,29 +112,29 @@
ns_log debug "OACS= sec_handler: enter"
if {[info exists ::security::log(login_cookie)]} {
- foreach c [list ad_session_id ad_secure_token ad_user_login ad_user_login_secure] {
- lappend msg "$c '[ad_get_cookie $c]'"
+ foreach c [list session_id secure_token user_login user_login_secure] {
+ lappend msg "$c '[ad_get_cookie [security::cookie_name $c]]'"
}
ns_log notice "OACS [ns_conn url] cookies: $msg"
}
try {
- ad_get_signed_cookie "ad_session_id"
+ ad_get_signed_cookie [security::cookie_name session_id]
} trap {AD_EXCEPTION NO_COOKIE} {errorMsg} {
#
# We have no session cookie. Maybe we are running under
# aa_test.
#
#if {[nsv_array exists aa_test]} {
- # ns_log notice "nsv_array logindata [nsv_get aa_test logindata logindata]"
- # ns_log notice "ns_conn peeraddr [ns_conn peeraddr]"
- # ns_log notice "dict get $logindata peeraddr [dict get $logindata peeraddr]"
+ # ns_log notice "... nsv_array logindata [nsv_get aa_test logindata logindata]"
+ # ns_log notice "... ns_conn peeraddr [ns_conn peeraddr]"
+ # ns_log notice "... dict get $logindata peeraddr [ns_conn peeraddr]"
#}
if {[nsv_array exists aa_test]
&& [nsv_get aa_test logindata logindata]
- && [ns_conn peeraddr] eq [dict get $logindata peeraddr]
+ && [ns_conn peeraddr] in [list [dict get $logindata peeraddr] 127.0.0.1 ::1]
} {
#ns_log notice logindata=$logindata
if {[dict exists $logindata user_id]} {
@@ -141,10 +146,10 @@
#ad_conn -set session_id [sec_allocate_session]
set auth_level ok
set untrusted_user_id $user_id
- set ::__aa_testing_mode 1
+ aa_test_start
}
}
- if {![info exists ::__aa_testing_mode]} {
+ if {![aa_test_running_p]} {
sec_login_handler
}
@@ -185,24 +190,33 @@
set persistent_login_p 0
if {$session_user_id > 0} {
- try {
- sec_login_read_cookie
+ set login_info [sec_login_read_cookie]
+ if {[dict get $login_info status] eq "OK"} {
- } trap {AD_EXCEPTION NO_COOKIE} {errorMsg} {
- #
- # No login cookie.
- #
- ns_log notice "=== no login_cookie"
+ set auth_token [dict get $login_info auth_token]
- } trap {AD_EXCEPTION INVALID_COOKIE} {errorMsg} {
#
- # Invalid login cookie (might be past validity)
+ # Verify currently stored user authentication token
+ # against the one on the login cookie.
#
- ns_log notice "=== invalid login_cookie"
-
- } on ok {login_list} {
- set login_cookie_exists_p 1
- set persistent_login_p [lindex $login_list end]
+ if {$auth_token ne [sec_get_user_auth_token $session_user_id]} {
+ #
+ # Invalid user auth token in the login
+ # cookie. This happens e.g. when user changed
+ # their password, hence all logins on different
+ # devices must be invalidated. Make sure to log
+ # the current user out and update session cookie
+ # and ad_conn information.
+ #
+ ad_user_logout
+ sec_login_handler
+ } else {
+ set login_cookie_exists_p 1
+ set persistent_login_p [dict get $login_info forever_p]
+ if {$persistent_login_p eq ""} {
+ set persistent_login_p 0
+ }
+ }
}
}
@@ -236,7 +250,7 @@
# after the logout this sesson_id is not accepted anymore,
# even when below sec_session_renew time (default 5min).
#
- ns_log warning "downgrade login_level since session_id was invalidated"
+ ns_log warning "downgrade login_level of user $session_user_id since session_id was invalidated"
set login_level 0
}
@@ -246,7 +260,7 @@
# login cookie, somebody tries to hack around.
#
set login_level 0
- ns_log warning "downgrade login_level since there is no login cookie provided"
+ ns_log warning "downgrade login_level of user $session_user_id since there is no login cookie provided"
}
switch -- $login_level {
@@ -287,7 +301,7 @@
&& ([security::secure_conn_p] || [ad_conn behind_secure_proxy_p])
} {
catch {
- set sec_token [split [ad_get_signed_cookie "ad_secure_token"] {,}]
+ set sec_token [split [ad_get_signed_cookie [security::cookie_name secure_token]] {,}]
if {[lindex $sec_token 0] eq $session_id
&& [lindex $sec_token 2] eq [ad_conn peeraddr]
} {
@@ -314,18 +328,7 @@
::security::log timeout "SessionRefresh in [expr {($session_expr - [sec_session_renew]) - [ns_time]}] secs"
if { $session_expr - [sec_session_renew] < [ns_time] } {
-
- # # LARS: We abandoned the use of sec_login_handler here. This lets people stay logged in forever
- # # if only they keep requesting pages frequently enough, but the alternative was that
- # # the situation where LoginTimeout = 0 (infinite) and the user unchecks the "Remember me" checkbox
- # # would cause users' sessions to expire as soon as the session needed to be renewed
sec_generate_session_id_cookie
-
- # apisano 2018-06-08: as discussed in
- # https://openacs.org/forums/message-view?message_id=1691183#msg_1691183,
- # this would break sec_change_user_auth_token as a mean to
- # invalidate user login...
- #sec_login_handler
}
}
#
@@ -361,27 +364,97 @@
ad_proc -private sec_login_read_cookie {} {
- Fetches values either from ad_user_login_secure or ad_user_login,
- depending whether we are in a secured connection or not.
+ Fetches values either from "user_login_secure" or "user_login"
+ cookies, depending whether we are in a secured connection or not.
@author Victor Guerra
- @return List of values read from cookie ad_user_login_secure or ad_user_login
+ @return dict of values from cookie "user_login_secure" or "user_login".
+ Additionally, the dict contains a member "status" with possible
+ values "OK", "NO_COOKIE" or "INVALID_COOKIE"
} {
#
+ # ad_user_login user_id,issue_time,auth_token,forever,external_registry
+ # ad_user_login_secure user_id,issue_time,auth_token,random,forever,external_registry
+ #
# If over HTTPS, we look for the *_secure cookie
#
if { [security::secure_conn_p] || [ad_conn behind_secure_proxy_p]} {
- set cookie_name "ad_user_login_secure"
+ set cookie_name [security::cookie_name user_login_secure]
+ set expect_elements 6
} else {
- set cookie_name "ad_user_login"
+ set cookie_name [security::cookie_name user_login]
+ set expect_elements 5
}
- return [split [ad_get_signed_cookie $cookie_name] ","]
+
+ #
+ # Provide default values for the result.
+ #
+ set result {
+ user_id 0
+ issue_time 0
+ auth_token ""
+ forever_p 0
+ external_registry ""
+ status NO_COOKIE
+ }
+
+ try {
+ ad_get_signed_cookie $cookie_name
+
+ } trap {AD_EXCEPTION NO_COOKIE} {errorMsg} {
+ dict set result status NO_COOKIE
+
+ } trap {AD_EXCEPTION INVALID_COOKIE} {errorMsg} {
+ dict set result status INVALID_COOKIE
+
+ } on ok {cookie_value} {
+ set login_list [split $cookie_value ","]
+ dict set result status OK
+ dict set result user_id [lindex $login_list 0]
+ dict set result issue_time [lindex $login_list 1]
+ dict set result auth_token [lindex $login_list 2]
+
+ if {[llength $login_list] == $expect_elements} {
+ dict set result forever_p [lindex $login_list end-1]
+ dict set result external_registry [lindex $login_list end]
+ } else {
+ #
+ # Legacy case (no external registry is provided). This is
+ # just needed for the transition phase, while still old
+ # cookies are in use, having no "external_registry"
+ # defined.
+ #
+ dict set result forever_p [lindex $login_list end]
+ dict set result external_registry ""
+ }
+ }
+ return $result
}
+ad_proc -public sec_login_get_external_registry {} {
-ad_proc -private sec_login_handler {} {
+ If the login was issued from an external_registry, use this as
+ well for refreshing.
+ @return registry object or the empty string when not applicable
+
+} {
+ set external_registry ""
+ if {[ns_conn isconnected]} {
+ set external_registry [dict get [sec_login_read_cookie] external_registry]
+ if {$external_registry ne "" && ![nsf::is object $external_registry]} {
+ ns_log warning "external registry object '$external_registry'" \
+ "used for login of user [ad_conn untrusted_user_id]" \
+ "does not exist. Ignored."
+ set external_registry ""
+ }
+ }
+ return $external_registry
+}
+
+ad_proc -public sec_login_handler {} {
+
If a login cookie exists, it is checked for expiration
(depending on LoginTimeout) and the account status is validated.
In every case, the session info including [ad_conn] and the
@@ -401,14 +474,8 @@
#
# Check login cookie.
#
- try {
- set login_list [sec_login_read_cookie]
- set login_info [list \
- user_id [lindex $login_list 0] \
- issue_time [lindex $login_list 1] \
- auth_token [lindex $login_list 2] \
- forever [lindex $login_list end] ]
-
+ set login_info [sec_login_read_cookie]
+ if {[dict get $login_info status] eq "OK"} {
set untrusted_user_id [dict get $login_info user_id]
set auth_level expired
@@ -436,6 +503,18 @@
} else {
set auth_level ok
}
+
+ #
+ # In case there is no session_id, do not trust the
+ # provided cookie, since it might be stolen. In
+ # general, session cookies are recreated on the fly
+ # for the current user, but we do not want this in
+ # cases, when we have already a "valid" login cookie.
+ #
+ if {[ad_conn session_id] eq ""} {
+ ns_log warning "downgrade auth_level of user $untrusted_user_id since session_id invalid"
+ set auth_level expired
+ }
} else {
ns_log notice "OACS= auth_token has changed"
}
@@ -451,16 +530,6 @@
set auth_level none
set account_status "closed"
}
- } trap {AD_EXCEPTION NO_COOKIE} {errorMsg} {
- #
- # There is no such such cookie, no error to report.
- #
- } trap {AD_EXCEPTION INVALID_COOKIE} {errorMsg} {
- #
- # The cookie is not valid (might be past validity)
- #
- } on error {errorMsg} {
- ns_log error "sec_login_handler: $errorMsg, $::errorCode"
}
sec_setup_session $untrusted_user_id $auth_level $account_status
@@ -470,6 +539,7 @@
ad_proc -public ad_user_login {
{-account_status "ok"}
{-cookie_domain ""}
+ {-external_registry ""}
-forever:boolean
user_id
} {
@@ -479,11 +549,14 @@
} {
set prev_user_id [ad_conn user_id]
- # deal with the permanent login cookies (ad_user_login and ad_user_login_secure)
+ #
+ # Deal with the permanent login cookies (user_login and
+ # user_login_secure).
+ #
if { $forever_p } {
set max_age inf
} else {
- # ad_user_login cookie will live for as long as the maximum login time
+ # user_login cookie will live for as long as the maximum login time
set max_age [sec_login_timeout]
}
@@ -493,35 +566,35 @@
set cookie_domain [parameter::get -parameter CookieDomain -package_id $::acs::kernel_id]
}
- # If you're logged in over a secure connection, you're secure
+ # If you're logged-in over a secure connection, you're secure
if { $secure_p } {
ad_set_signed_cookie \
-max_age $max_age \
-secure t \
-domain $cookie_domain \
- ad_user_login_secure \
- "$user_id,[ns_time],[sec_get_user_auth_token $user_id],[ns_time],$forever_p"
+ [security::cookie_name user_login_secure] \
+ "$user_id,[ns_time],[sec_get_user_auth_token $user_id],[ns_time],$forever_p,$external_registry"
# We're secure
set auth_level "secure"
} elseif { $prev_user_id != $user_id } {
# Hose the secure login token if this user is different
# from the previous one.
- ad_unset_cookie -secure t ad_user_login_secure
+ ad_unset_cookie -secure t [security::cookie_name user_login_secure]
}
#
- # Set "ad_user_login" Cookie always with secure=f for mixed
+ # Set "user_login" Cookie always with secure=f for mixed
# content.
#
- ns_log Debug "ad_user_login: Setting new ad_user_login cookie with max_age $max_age"
+ ns_log Debug "ad_user_login: Setting new user_login cookie with max_age $max_age"
ad_set_signed_cookie \
-expire [expr {$forever_p ? false : true}] \
-max_age $max_age \
-domain $cookie_domain \
-secure f \
- ad_user_login \
- "$user_id,[ns_time],[sec_get_user_auth_token $user_id],$forever_p"
+ [security::cookie_name user_login] \
+ "$user_id,[ns_time],[sec_get_user_auth_token $user_id],$forever_p,$external_registry"
# deal with the current session
sec_setup_session -cookie_domain $cookie_domain $user_id $auth_level $account_status
@@ -547,7 +620,8 @@
ad_proc -public sec_change_user_auth_token {
user_id
} {
- Change the user's auth_token, which invalidates all existing login cookies, i.e. forces user logout at the server.
+ Change the user's auth_token, which invalidates all existing login cookies,
+ i.e. forces user logout at the server.
} {
set auth_token [ad_generate_random_string]
@@ -559,14 +633,29 @@
return $auth_token
}
-
ad_proc -public ad_user_logout {
{-cookie_domain ""}
} {
Logs the user out.
} {
+
+ set external_registry [sec_login_get_external_registry]
+ if {$external_registry ne ""} {
+ #
+ # If we were logged in via an external identity provider, try
+ # to logout from there as well. Note that not every external
+ # identity provider supports a logout (e.g. GitHub), and maybe
+ # in some cases, the external logout is not wanted. This
+ # should be provided by the implementation of the external
+ # registry.
+ #
+ $external_registry logout
+ }
+
if {$cookie_domain eq ""} {
- set cookie_domain [parameter::get -parameter CookieDomain -package_id $::acs::kernel_id]
+ set cookie_domain [parameter::get \
+ -parameter CookieDomain \
+ -package_id $::acs::kernel_id]
}
#
@@ -581,54 +670,220 @@
# "SecureSessionCookie" was altered during a session, but this
# should be a seldom border case.
#
- ad_unset_cookie -domain $cookie_domain -secure [expr {[parameter::get \
+ ad_unset_cookie \
+ -domain $cookie_domain \
+ -secure [expr {[parameter::get \
+ -boolean \
-parameter SecureSessionCookie \
- -package_id [ad_acs_kernel_id] \
- -default 0] ? "t" : "f"}] ad_session_id
- ad_unset_cookie -domain $cookie_domain -secure f ad_user_login
- ad_unset_cookie -domain $cookie_domain -secure t ad_secure_token
- ad_unset_cookie -domain $cookie_domain -secure t ad_user_login_secure
+ -package_id $::acs::kernel_id \
+ -default 0] ? "t" : "f"}] \
+ [security::cookie_name session_id]
+
+ set external_registry [dict get [sec_login_read_cookie] external_registry]
+ if {$external_registry ne "" && [nsf::is object $external_registry]} {
+ #
+ # Logout from external registry
+ #
+ ns_log notice "logout from external registry: $external_registry"
+ $external_registry logout
+ }
+
+ ad_unset_cookie -domain $cookie_domain -secure f [security::cookie_name user_login]
+ ad_unset_cookie -domain $cookie_domain -secure t [security::cookie_name secure_token]
+ ad_unset_cookie -domain $cookie_domain -secure t [security::cookie_name user_login_secure]
}
+namespace eval ::security {
+ ad_proc -private preferred_password_hash_algorithm {} {
+
+ Check the list of preferred password hash algorithms and the
+ return the best which is available (or "salted-sha1" if
+ nothing applies).
+
+ @return password preferred hash algorithm
+ } {
+
+ set preferences [parameter::get \
+ -parameter PasswordHashAlgorithm \
+ -package_id $::acs::kernel_id \
+ -default "salted-sha1"]
+ foreach algo $preferences {
+ if {[info commands ::security::hash::$algo] ne ""} {
+ #
+ # This preference is available.
+ #
+ return $algo
+ } else {
+ ns_log warning "PasswordHashAlgorithm '$algo' was specified," \
+ "but is not available in your setup."
+ }
+ }
+ #
+ # General fallback (only necessary for invalid parameter settings)
+ #
+ ns_log warning "No valid PasswordHashAlgorithm was specified: '$preferences'." \
+ "Fall back to default."
+
+ return "salted-sha1"
+ }
+}
+
+namespace eval ::security::hash {
+ ad_proc -private salted-sha1 {password salt} {
+
+ Classical OpenACS password hash algorithm. This algorithm must
+ be always available and is independent of the
+ NaviServer/AOLserver version.
+
+ @return hex encoded password hash
+
+ } {
+ set salt [string trim $salt]
+ return [ns_sha1 ${password}${salt}]
+ }
+
+ if {[::acs::icanuse "ns_crypto::pbkdf2_hmac"]} {
+ ad_proc -private scram-sha-256 {password salt} {
+
+ SCRAM hash function using sha256 as digest function. The
+ SCRAM hash function is PBKDF2 [RFC2898] with HMAC as the
+ pseudo-random function and where the output key length ==
+ hash length. We use 15K iterations for PBKDF2 as
+ recommended in RFC 7677.
+
+ @return hex encoded password hash (64 bytes)
+ } {
+ return [::ns_crypto::pbkdf2_hmac \
+ -digest sha256 \
+ -iterations 15000 \
+ -secret $password \
+ -salt $salt]
+ }
+ }
+
+ if {[::acs::icanuse "ns_crypto::scrypt"]} {
+ ad_proc -private scrypt-16384-8-1 {password salt} {
+
+ Compute a "password hash" using the scrypt password based
+ key derivation function (RFC 7914)
+
+ @return hex encoded password hash (128 bytes)
+ } {
+ return [::ns_crypto::scrypt -secret $password -salt $salt -n 16384 -r 8 -p 1]
+ }
+ }
+
+ if {[::acs::icanuse "ns_crypto::argon2"]} {
+ ad_proc -private argon2-12288-3-1 {password salt} {
+
+ Compute a "password hash" using the Argon2 hash algorithm
+ key derivation function (RFC 9106).
+
+ Parameterization recommendation from OWASP: m=12288 (12 MiB), t=3, p=1
+
+ @return hex encoded password hash (128 bytes)
+ } {
+ return [::ns_crypto::argon2 -variant argon2id \
+ -password $password -salt $salt \
+ -memcost 12288 -iter 3 -lanes 1 -threads 1 -outlen 64]
+ }
+
+ ad_proc -private argon2-rfc9106-high-mem {password salt} {
+
+ Compute a "password hash" using the Argon2 hash algorithm
+ key derivation function (RFC 9106).
+
+ Parameterization first recommendation from RFC 9106:
+ t=1, m=2GiB, p=4 (2 GiB = 2,097,152 KB)
+
+ @return hex encoded password hash (128 bytes)
+ } {
+ return [::ns_crypto::argon2 -variant argon2id \
+ -password $password -salt $salt \
+ -memcost 2097152 -iter 1 -lanes 4 -threads 4 -outlen 64]
+ }
+
+ ad_proc -private argon2-rfc9106-low-mem {password salt} {
+
+ Compute a "password hash" using the Argon2 hash algorithm
+ key derivation function (RFC 9106).
+
+ Parameterization second recommendation from RFC 9106 (low memory):
+ t=3, m=64 MiB, p=4 (64 MiB = 65,536 KB)
+
+ @return hex encoded password hash (128 bytes)
+ } {
+ return [::ns_crypto::argon2 -variant argon2id \
+ -password $password -salt $salt \
+ -memcost 65536 -iter 3 -lanes 4 -threads 4 -outlen 64]
+ }
+
+ }
+}
+
ad_proc -public ad_check_password {
user_id
password_from_form
} {
- Returns 1 if the password is correct for the given user ID.
+
+ Check if the provided password is correct. OpenACS never stores
+ password, but uses salted hashes for identification. Different
+ algorithm can be used. When the stored hash is from another hash
+ algorithm, which is preferred, this function updates the password
+ hash automatically, but only, when the password is correct.
+
+ @return Returns 1 if the password is correct for the given user ID.
} {
- set found_p [db_0or1row password_select {select password, salt from users where user_id = :user_id}]
- db_release_unused_handles
+ set found_p [db_0or1row password_select {
+ select password, salt, password_hash_algorithm from users where user_id = :user_id
+ }]
if { !$found_p } {
return 0
}
- set salt [string trim $salt]
-
- if {$password ne [ns_sha1 "$password_from_form$salt"] } {
+ if {$password ne [::security::hash::$password_hash_algorithm $password_from_form $salt] } {
return 0
}
+ set preferred_hash_algorithm [security::preferred_password_hash_algorithm]
+ if {$preferred_hash_algorithm ne $password_hash_algorithm} {
+ ns_log notice "upgrade password hash for user $user_id from" \
+ "$password_hash_algorithm to $preferred_hash_algorithm"
+ ad_change_password \
+ -password_hash_algorithm $preferred_hash_algorithm \
+ $user_id \
+ $password_from_form
+ }
return 1
}
ad_proc -public ad_change_password {
+ {-password_hash_algorithm "salted-sha1"}
user_id
new_password
} {
Change the user's password
} {
- # In case someone wants to change the salt from now on, you can do
- # this and still support old users by changing the salt below.
-
if { $user_id eq "" } {
error "No user_id supplied"
}
+ #
+ # The hash algorithms are called in standard OpenACS with a salt
+ # size of 20 bytes (in hex format), which corresponds to 160-bit.
+ #
set salt [sec_random_token]
- set new_password [ns_sha1 "$new_password$salt"]
- db_dml password_update {}
- db_release_unused_handles
+ set new_password [::security::hash::$password_hash_algorithm $new_password $salt]
+
+ db_dml password_update {
+ update users
+ set password = :new_password,
+ salt = :salt,
+ password_hash_algorithm = :password_hash_algorithm,
+ password_changed_date = current_timestamp
+ where user_id = :user_id
+ }
}
ad_proc -private sec_setup_session {
@@ -653,7 +908,7 @@
ns_log debug "OACS= empty session_id"
set session_id [sec_allocate_session]
- # if we have a user on an newly allocated session, update
+ # if we have a user on a newly allocated session, update
# users table
ns_log debug "OACS= newly allocated session $session_id"
@@ -664,40 +919,44 @@
ns_log debug "OACS= done updating user session info, user_id NONZERO"
}
} else {
- # $session_id is an active verified session
- # this call is either a user logging in
- # on an active unidentified session, or a change in identity
- # for a browser that is already logged in
-
- # this is an active session [ad_conn user_id] will not return
- # the empty string
+ #
+ # $session_id is an active verified session this call is
+ # either a user doing a log-in on an active unidentified
+ # session, or a change in identity for a browser that is
+ # already logged-in.
+ #
set prev_user_id [ad_conn user_id]
#
# Change the session id for all user_id changes, also on
# changes from user_id 0, since owasp recommends to renew the
- # session_id after any privilege level change
+ # session_id after any privilege level change.
#
ns_log debug "prev_user_id $prev_user_id new_user_id $new_user_id"
if { $prev_user_id != 0 && $prev_user_id != $new_user_id } {
- # this is a change in identity so we should create
- # a new session so session-level data is not shared
- ns_log debug "sec_allocate_session"
+ #
+ # This is a change in identity so we create
+ # a new session_id to avoid sharing of session-level data
+ #
set session_id [sec_allocate_session]
}
if { $prev_user_id != $new_user_id } {
- # a change of user_id on an active session
- # demands an update of the users table
+ #
+ # A change of user_id on an active session demands an
+ # update of the users table.
+ #
ns_log debug "sec_update_user_session_info"
sec_update_user_session_info $new_user_id
}
}
set user_id 0
-
- # If both auth_level and account_status are 'ok' or better, we have a solid user_id
+ #
+ # If both auth_level and account_status are 'ok' or better, we
+ # have a solid user_id.
+ #
if { ($auth_level eq "ok" || $auth_level eq "secure") && $account_status eq "ok" } {
set user_id $new_user_id
}
@@ -719,8 +978,10 @@
&& ([security::secure_conn_p] || [ad_conn behind_secure_proxy_p])
&& $new_user_id != 0
} {
- # this is a secure session, so the browser needs
- # a cookie marking it as such
+ #
+ # This is a secure session, so the browser needs
+ # a cookie marking it as such.
+ #
sec_generate_secure_token_cookie
}
}
@@ -736,10 +997,21 @@
db_release_unused_handles
}
+ad_proc security::cookie_name {plain_name} {
+ @return the supplied cookie name, but potentially prefixed
+ according to the NaviServer CookieNamespace parameter, to
+ make it unique for this particular domain.
+} {
+ #
+ # Setting a cookie always requires a connection.
+ #
+ return [ns_config "ns/server/[ns_info server]/acs" CookieNamespace "ad_"]$plain_name
+}
+
ad_proc -private sec_generate_session_id_cookie {
{-cookie_domain ""}
} {
- Sets the ad_session_id cookie based on global variables.
+ Sets the "session_id" cookie based on global variables.
} {
set user_id [ad_conn untrusted_user_id]
#
@@ -758,41 +1030,50 @@
}
}
- ns_log Debug "Security: [ns_time] sec_generate_session_id_cookie setting session_id=$session_id, user_id=$user_id, login_level=$login_level"
+ ns_log Debug "Security: [ns_time] sec_generate_session_id_cookie setting" \
+ "session_id=$session_id, user_id=$user_id, login_level=$login_level"
if {$cookie_domain eq ""} {
- set cookie_domain [parameter::get -parameter CookieDomain -package_id $::acs::kernel_id]
+ set cookie_domain [parameter::get \
+ -parameter CookieDomain \
+ -package_id $::acs::kernel_id]
}
- # Fetch the last value element of ad_user_login cookie (or
- # ad_user_login_secure) that indicates if user wanted to be
+ # Fetch the last value element of "user_login" or
+ # "user_login_secure" cookie that indicates if user wanted to be
# remembered when logging in.
set discard t
set max_age [sec_session_timeout]
- catch {
- set login_list [sec_login_read_cookie]
- if {[lindex $login_list end] == 1} {
- set discard f
- set max_age inf
- }
+ set login_info [sec_login_read_cookie]
+ if {[dict get $login_info status] eq "OK"
+ && [dict get $login_info forever_p]
+ } {
+ set discard f
+ set max_age inf
}
+
ad_set_signed_cookie \
-secure [expr {[parameter::get \
+ -boolean \
-parameter SecureSessionCookie \
- -package_id [ad_acs_kernel_id] \
+ -package_id $::acs::kernel_id \
-default 0] ? "t" : "f"}] \
-discard $discard \
-replace t \
-max_age $max_age \
-domain $cookie_domain \
- ad_session_id "$session_id,$user_id,$login_level,[ns_time]"
+ [security::cookie_name session_id] \
+ "$session_id,$user_id,$login_level,[ns_time]"
}
ad_proc -private sec_generate_secure_token_cookie { } {
- Sets the ad_secure_token cookie.
+ Sets the "secure_token" cookie.
} {
- ad_set_signed_cookie -secure t "ad_secure_token" "[ad_conn session_id],[ns_time],[ad_conn peeraddr]"
+ ad_set_signed_cookie \
+ -secure t \
+ [security::cookie_name secure_token] \
+ "[ad_conn session_id],[ns_time],[ad_conn peeraddr]"
}
ad_proc -private sec_allocate_session {} {
@@ -823,7 +1104,6 @@
set url [ad_conn url]
if { [string match "*register/*" $url]
|| [string match "/index*" $url]
- || [string match "/index*" $url]
|| "/" eq $url
|| [string match "*password-update*" $url]
} {
@@ -855,11 +1135,12 @@
# assumes that the host-node-map is always short. This allows us
# as well to purge the entries without a pattern match.
#
- set lists [db_list_of_lists -cache_key ad_get_host_node_map \
- get_node_host_names {select host, node_id from host_node_map}]
- set p [lsearch -index 0 -exact $lists $hostname]
+ set mapping [acs::misc_cache eval ad_get_host_node_map {
+ db_list_of_lists get_node_host_names {select host, node_id from host_node_map}
+ }]
+ set p [lsearch -index 0 -exact $mapping $hostname]
if {$p != -1} {
- set result [lindex $lists $p 1]
+ set result [lindex $mapping $p 1]
} else {
set result 0
}
@@ -910,7 +1191,7 @@
return $location/[join $elements /]
}
-ad_proc -private security::get_register_subsite {} {
+ad_proc security::get_register_subsite {} {
Returns a URL pointing to the subsite, on which the
register/unregister should be performed. If there is no current
@@ -954,9 +1235,9 @@
set url [subsite::get_element -subsite_id $package_id -element url]
set url [security::get_qualified_url $url]
# We have a fully qualified url, but we have to remap
- # the URL to the configured host name, since
+ # the URL to the configured hostname, since
# get_qualified prepends the [ad_conn location], which
- # points to the virtual host name.
+ # points to the virtual hostname.
set url [security::replace_host_in_url -hostname $config_hostname $url]
}
} else {
@@ -979,11 +1260,11 @@
#
# We are on normal subsite
#
- if { [ad_conn isconnected] } {
+ if { [ns_conn isconnected] } {
set url [subsite::get_element -element url]
#
# Check to see that the user (most likely "The Public"
- # party, since there's probably no user logged in)
+ # party, since there's probably no user logged-in)
# actually have permission to view that subsite, otherwise
# we'll get into an infinite redirect loop.
#
@@ -1017,54 +1298,155 @@
host_node_id $host_node_id]
}
+ad_proc security::safe_tmpfile_p {
+ -must_exist:boolean
+ tmpfile
+} {
+ Checks that a file is a safe tmpfile, that is, it belongs to the
+ configured tmpdir.
+
+ When the file exists, we also enforce additional criteria:
+ - file must belong to the current system user
+ - file must be readable and writable by the current system user
+
+ @param tmpfile absolute path to a possibly existing tmpfile
+ @param must_exist make sure the file exists
+
+ @return boolean
+} {
+ #
+ # Ensure no ".." in the path
+ #
+ set tmpfile [ns_normalizepath $tmpfile]
+ set tmpdir [string trimright [ns_config ns/parameters tmpdir] /]
+
+ if {[ad_file dirname $tmpfile] ne $tmpdir} {
+ #
+ # File is not a direct child of the tmpfolder: not safe
+ #
+ return false
+ }
+
+ if {![ad_file exists $tmpfile]} {
+ #
+ # File does not exist yet: safe, unless we demand for the file
+ # to exist.
+ #
+ return [expr {!$must_exist_p}]
+ }
+
+ if {![ad_file owned $tmpfile]} {
+ #
+ # File does not belong to us: not safe
+ #
+ return false
+ }
+
+ if {![ad_file readable $tmpfile]} {
+ #
+ # We cannot read the file: not safe
+ #
+ return false
+ }
+
+ if {![ad_file writable $tmpfile]} {
+ #
+ # We cannot write the file: not safe
+ #
+ return false
+ }
+
+ #
+ # The file is safe
+ #
+ return true
+}
+
ad_proc -public ad_get_login_url {
{-authority_id ""}
{-username ""}
-return:boolean
+ {-external_registry ""}
} {
- Returns a URL to the login page of the closest subsite, or the main site, if there's no current connection.
+ Returns a URL to the login page of the closest subsite, or the
+ main site, if there's no current connection.
- @option return If set, will export the current form, so when the registration is complete,
- the user will be returned to the current location. All variables in
- ns_getform (both posts and gets) will be maintained.
+ @option return If set, will export the current form, so when
+ the registration is complete, the user will be returned
+ to the current location. All variables in
+ ns_getform (both posts and gets) will be maintained.
@author Lars Pind (lars@collaboraid.biz)
@author Gustaf Neumann
} {
+ #
+ # Get the login_url 'url' and some more parameters form the
+ # register subsite for this registry.
+ #
set subsite_info [security::get_register_subsite]
foreach var {url require_qualified_return_url host_node_id} {
set $var [dict get $subsite_info $var]
}
- append url "register/"
-
- #
- # Don't add a return_url if you're already under /register,
- # because that will frequently interfere with the normal login
- # procedure.
- #
- if { [ad_conn isconnected] && $return_p && ![string match "register/*" [ad_conn extra_url]] } {
+ if { [ns_conn isconnected]
+ && $return_p
+ } {
#
# In a few cases, we do not need to add a fully qualified
# return url. The secure cases have to be still tested.
#
if { !$require_qualified_return_url
- && ([security::secure_conn_p] || [ad_conn behind_secure_proxy_p] || ![security::RestrictLoginToSSLP])
+ && ([security::secure_conn_p]
+ || [ad_conn behind_secure_proxy_p]
+ || ![security::RestrictLoginToSSLP]
+ )
} {
set return_url [ad_return_url]
} else {
set return_url [ad_return_url -qualified]
}
}
- if {$host_node_id == 0} {
- unset host_node_id
- }
- set url [export_vars -base $url -no_empty {authority_id username return_url host_node_id}]
+ if {$external_registry ne ""} {
+ ns_log notice "the external registry $external_registry is used"
+ #
+ # We get here in cases of a refresh of a login, since we know
+ # that the current user_id is expired, and the user has
+ # registered via an external registry. Therefore, we use
+ # the same external registry for the refresh.
+ #
+ # In general, we have two options: (a) redirect directly to
+ # the external registry login page, or (b) redirect to an
+ # external registry enhanced classical OpenACS login page. We
+ # are here on the (a) path, since potentially, the external
+ # identity managers allows one to continue without even showing a
+ # login page (when it says, the login is still valid).
+ #
+ # The path (b) might be chosen via a future package parameter.
+ #
+ set url [$external_registry login_url -return_url $return_url]
+ } else {
+ append url "register/"
+
+ #
+ # Don't add a return_url if you're already under /register,
+ # because that will frequently interfere with the normal login
+ # procedure.
+ #
+ if { [string match "register/*" [ad_conn extra_url]] } {
+ set return_url ""
+ }
+ if {$host_node_id == 0} {
+ unset host_node_id
+ }
+ set url [export_vars -base $url -no_empty {
+ authority_id username return_url host_node_id
+ }]
+ }
::security::log login_url "ad_get_login_url: final login_url <$url>"
return $url
@@ -1075,9 +1457,10 @@
{-return_url ""}
} {
- Returns a URL to the logout page of the closest subsite, or the main site, if there's no current connection.
+ Returns a URL to the logout page of the closest subsite, or the
+ main site, if there's no current connection.
- @option return If set, will export the current form, so when the logout is complete
+ @option return If set, will export the current form, so when the logout is complete
the user will be returned to the current location. All variables in
ns_getform (both posts and gets) will be maintained.
@@ -1099,6 +1482,47 @@
return $url
}
+ad_proc -public ad_get_external_registries {
+ {-subsite_id ""}
+} {
+
+ Return for the specified subsite (or the current registry subsite)
+ the external authority interface objs. Per default, all defined
+ external registries are returned, but a subsite might restrict this.
+
+} {
+ if {$subsite_id eq ""} {
+ set subsite_id [dict get [security::get_register_subsite] subsite_id]
+ }
+ set offered_registries [parameter::get \
+ -parameter OfferedRegistries \
+ -package_id $subsite_id \
+ -default *]
+
+ set result {}
+ if {[nsf::is object ::xo::Authorize]} {
+ foreach auth_obj [::xo::Authorize info instances -closure] {
+ #
+ # Don't list on the general available pages the external
+ # authorization objects when these are configured in debugging
+ # mode.
+ #
+ if {[$auth_obj cget -debug]} {
+ continue
+ }
+
+ if {$offered_registries eq "*"
+ || $auth_obj in $offered_registries
+ } {
+ lappend result $auth_obj
+ }
+ }
+ }
+ return $result
+}
+
+
+
# JCD 20020915 I think this probably should not be deprecated since it is
# far more reliable than permissioning esp for a development server
@@ -1148,6 +1572,7 @@
{-secret ""}
{-token_id ""}
{-max_age ""}
+ {-binding 0}
value
} {
Returns a digital signature of the value. Negative token_ids are
@@ -1161,8 +1586,18 @@
@param secret allows the caller to specify a known secret external
to the random secret management mechanism.
- @param token_id allows the caller to specify a token_id which is then ignored so don't use it.
+ @param token_id allows the caller to specify a token_id which
+ is then ignored so don't use it.
+ @param binding allows the caller to bind a signature to a user/session.
+ A value of 0 (default) means no additional binding.
+ When the value is "-1" only the user who created the signature can
+ obtain the value again.
+ When the value is "-2" only the user with the same csrf token can
+ obtain the value again.
+
+ The permissible values might be extended in the future.
+
@param value the value to be signed.
} {
if {$token_id eq ""} {
@@ -1185,7 +1620,22 @@
set expire_time [expr {$max_age + [ns_time]}]
}
- set hash [ns_sha1 "$value$token_id$expire_time$secret_token"]
+ switch $binding {
+ -1 {
+ set binding_value [ad_conn user_id]
+ append token_id :$binding
+ }
+ -2 {
+ set binding_value [::security::csrf::new]
+ append token_id :$binding
+ }
+ 0 {
+ set binding_value ""
+ }
+ default {error "invalid binding"}
+ }
+
+ set hash [ns_sha1 "$value$token_id$expire_time$secret_token$binding_value"]
set signature [list $token_id $expire_time $hash]
return $signature
@@ -1203,6 +1653,10 @@
@param secret specifies an external secret to use instead of the
one provided by the ACS signature mechanism.
} {
+ if {![string is list $signature]} {
+ ns_log warning "signature is not a list '$signature'"
+ return 0
+ }
lassign $signature token_id expire_time hash
return [__ad_verify_signature $value $token_id $secret $expire_time $hash]
}
@@ -1218,6 +1672,10 @@
@param secret specifies an external secret to use instead of the
one provided by the ACS signature mechanism.
} {
+ if {![string is list $signature]} {
+ ns_log warning "signature is not a list '$signature'"
+ return 0
+ }
lassign $signature token_id expire_time hash
if { [__ad_verify_signature $value $token_id $secret $expire_time $hash] } {
return $expire_time
@@ -1239,26 +1697,44 @@
} {
+ lassign [split $token_id :] raw_token_id binding
+
if { $secret eq "" } {
- if { $token_id eq "" } {
+ if { $raw_token_id eq "" } {
ns_log Debug "__ad_verify_signature: Neither secret, nor token_id supplied"
return 0
- } elseif {![string is integer -strict $token_id]} {
- ns_log Warning "__ad_verify_signature: token_id <$token_id> is not an integer"
+ } elseif {![string is integer -strict $raw_token_id]} {
+ ns_log Warning "__ad_verify_signature: token_id <$raw_token_id> is not an integer"
return 0
}
- set secret_token [sec_get_token $token_id]
+ try {
+ set secret_token [sec_get_token $raw_token_id]
+ } on error {errmsg} {
+ ns_log Warning "__ad_verify_signature: token_id <$raw_token_id> validation returns '$errmsg'"
+ return 0
+ }
+
} else {
set secret_token $secret
}
ns_log Debug "__ad_verify_signature: Getting token_id $token_id, value $secret_token ; "
ns_log Debug "__ad_verify_signature: Expire_Time is $expire_time (compare to [ns_time]), hash is $hash"
- # validate cookie: verify hash and expire_time
- set computed_hash [ns_sha1 "$value$token_id$expire_time$secret_token"]
+ if {$binding == -1} {
+ set binding_value [ad_conn user_id]
+ } elseif {$binding == -2} {
+ set binding_value [::security::csrf::new]
+ } else {
+ set binding_value ""
+ }
+ #
+ # Compute hash based on tokes, expire_time and user_id/csrf token
+ #
+ set computed_hash [ns_sha1 "$value$token_id$expire_time$secret_token$binding_value"]
+
# Need to verify both hash and expiration
set hash_ok_p 0
set expiration_ok_p 0
@@ -1267,14 +1743,20 @@
ns_log Debug "__ad_verify_signature: Hash matches - Hash check OK"
set hash_ok_p 1
} else {
- # check to see if IE is lame (and buggy!) and is expanding \n to \r\n
+ #
+ # Check to see if IE is lame (and buggy!) and is expanding \n to \r\n
# See: http://rhea.redhat.com/bboard-archive/webdb/000bfF.html
+ #
set value [string map [list \r ""] $value]
set org_computed_hash $computed_hash
- set computed_hash [ns_sha1 "$value$token_id$expire_time$secret_token"]
+ set computed_hash [ns_sha1 "$value$token_id$expire_time$secret_token$binding_value"]
if {$computed_hash eq $hash} {
- ns_log Debug "__ad_verify_signature: Hash matches after correcting for IE bug - Hash check OK"
+ #
+ # Not sure, the comments for IE are still true, so issue
+ # warnings in the error.log when this happens.
+ #
+ ns_log warning "__ad_verify_signature: Hash matches after correcting for IE bug - Hash check OK"
set hash_ok_p 1
} else {
ns_log Debug "__ad_verify_signature: Hash ($hash) doesn't match what we expected ($org_computed_hash) - Hash check FAILED"
@@ -1308,10 +1790,14 @@
validation fails (maybe due to expiration).
@return cookie value
+
+ @see ad_get_cookie
+ @see ad_set_signed_cookie
+ @see ad_get_signed_cookie_with_expr
} {
set cookie_value [ad_get_cookie -include_set_cookies $include_set_cookies $name]
- if { $cookie_value eq "" } {
+ if { $cookie_value eq "" || ![string is list $cookie_value]} {
throw {AD_EXCEPTION NO_COOKIE} {Cookie does not exist}
}
@@ -1335,14 +1821,18 @@
Retrieves a signed cookie. Validates a cookie against its
cryptographic signature and ensures that the cookie has not
- expired. Returns a two-element list, the first element of which is
- the cookie data, and the second element of which is the expiration
- time. Throws an exception if validation fails.
+ expired. Throws an exception when cookie does not exist or
+ validation fails.
+ @return Two-element list containing cookie data and expiration time
+
+ @see ad_get_cookie
+ @see ad_get_signed_cookie
+ @see ad_set_signed_cookie
} {
set cookie_value [ad_get_cookie -include_set_cookies $include_set_cookies $name]
- if { $cookie_value eq "" } {
+ if { $cookie_value eq "" || ![string is list $cookie_value]} {
throw {AD_EXCEPTION NO_COOKIE} {Cookie does not exist}
}
@@ -1370,6 +1860,7 @@
{-path "/"}
{-secret ""}
{-token_id ""}
+ {-samesite lax}
name
value
} {
@@ -1396,6 +1887,10 @@
@param value the value for the cookie. This is automatically
url-encoded.
+ @see ad_set_cookie
+ @see ad_get_signed_cookie
+ @see ad_get_signed_cookie_with_expr
+
} {
if { $signature_max_age eq "" } {
if { $max_age in {"inf" 0} } {
@@ -1425,6 +1920,7 @@
-max_age $max_age \
-domain $domain \
-path $path \
+ -samesite $samesite \
$name $data
}
@@ -1438,7 +1934,33 @@
#
#####
-ad_proc -private sec_get_token {
+if {[ns_info name] eq "NaviServer"} {
+ ad_proc -private sec_get_token_from_nsv {token_id token_var} {
+
+ Just for compatibility with AOLserver, which does not support
+ an atomic check and get operation for nsv.
+
+ } {
+ upvar $token_var token
+ return [nsv_get secret_tokens $token_id token]
+ }
+} else {
+ ad_proc -private sec_get_token_from_nsv {token_id token_var} {
+
+ Compatibility function for AOLserver, which does not support
+ nsv_get with the optional output variable.
+
+ } {
+ upvar $token_var token
+ if {[nsv_exists secret_tokens $token_id]} {
+ set token [nsv_get secret_tokens $token_id]
+ return 1
+ }
+ return 0
+ }
+}
+
+ad_proc -public sec_get_token {
token_id
} {
@@ -1454,92 +1976,109 @@
} {
+ #
+ # First check the per-thread cache to obtain a token from the
+ # token_id.
+ #
set key ::security::tcl_secret_tokens($token_id)
- if { [info exists $key] } { return [set $key] }
+ if { [info exists $key] } {
+ return [set $key]
+ }
+ #
+ # If there is no secret token available per thread,
+ # get it and try again.
+ #
if {[array size ::security::tcl_secret_tokens] == 0} {
- populate_secret_tokens_thread_cache
- if { [info exists $key] } { return [set $key] }
+ sec_populate_secret_tokens_thread_cache
+ if { [info exists $key] } {
+ return [set $key]
+ }
}
#
# We might get token_ids from previous runs, so we have fetch these
- # in case of a validation
+ # from the secret tokens cache, or from the data base.
#
- set token [ns_cache eval secret_tokens $token_id {
+ if {![sec_get_token_from_nsv $token_id token]} {
set token [db_string get_token {select token from secret_tokens
where token_id = :token_id} -default 0]
-
- # Very important to throw the error here if $token == 0
-
- if { $token == 0 } {
+ if {$token ne 0} {
+ nsv_set secret_tokens $token_id $token
+ } else {
+ #
+ # Very important to throw the error here if $token == 0
+ #
error "Invalid token ID"
}
+ }
- return $token
- }]
-
set $key $token
return $token
}
-ad_proc -private sec_get_random_cached_token_id {} {
+ad_proc -public sec_get_random_cached_token_id {} {
Randomly returns a token_id from the token cache
} {
#set list_of_names [ns_cache names secret_tokens]
set list_of_names [array names ::security::tcl_secret_tokens]
if {[llength $list_of_names] == 0} {
- populate_secret_tokens_thread_cache
+ sec_populate_secret_tokens_thread_cache
set list_of_names [array names ::security::tcl_secret_tokens]
}
set random_seed [ns_rand [llength $list_of_names]]
return [lindex $list_of_names $random_seed]
}
-ad_proc -private populate_secret_tokens_thread_cache {} {
+ad_proc -private sec_populate_secret_tokens_thread_cache {} {
Copy secret_tokens cache to per-thread variables
} {
- set ids [ns_cache names secret_tokens]
- if {[llength $ids] == 0} {
- populate_secret_tokens_cache
- set ids [ns_cache names secret_tokens]
+ set secret_tokens [nsv_array get secret_tokens]
+ if {[llength $secret_tokens] == 0} {
+ sec_populate_secret_tokens_cache
+ set secret_tokens [nsv_array get secret_tokens]
}
- foreach name $ids {
- set ::security::tcl_secret_tokens($name) [ns_cache get secret_tokens $name]
+ foreach {id token} $secret_tokens {
+ set ::security::tcl_secret_tokens($id) $token
}
}
-ad_proc -private populate_secret_tokens_cache {} {
+ad_proc -private sec_populate_secret_tokens_cache {} {
Randomly populates the secret_tokens cache.
} {
+ set num_tokens [parameter::get \
+ -package_id $::acs::kernel_id \
+ -parameter NumberOfCachedSecretTokens \
+ -default 100]
- set num_tokens [parameter::get -package_id [ad_acs_kernel_id] -parameter NumberOfCachedSecretTokens -default 100]
-
# this is called directly from security-init.tcl,
# so it runs during the install before the data model has been loaded
if { [db_table_exists secret_tokens] } {
db_foreach get_secret_tokens {} {
- ns_cache set secret_tokens $token_id $token
+ nsv_set secret_tokens $token_id $token
}
}
db_release_unused_handles
}
-ad_proc -private populate_secret_tokens_db {} {
+ad_proc -private sec_populate_secret_tokens_db {} {
Populates the secret_tokens table. Note that this will take a while
to run.
} {
- set num_tokens [parameter::get -package_id [ad_acs_kernel_id] -parameter NumberOfCachedSecretTokens -default 100]
+ set num_tokens [parameter::get \
+ -package_id $::acs::kernel_id \
+ -parameter NumberOfCachedSecretTokens \
+ -default 100]
# we assume sample size of 10%.
set num_tokens [expr {$num_tokens * 10}]
set counter 0
@@ -1566,14 +2105,14 @@
#
#####
-ad_proc -private sec_lookup_property {
+ad_proc -private sec_lookup_property_not_cached {
id
module
name
} {
- Used as a helper procedure for util_memoize to look up a
- particular property from the database.
+ Look up a particular session property from the database and record
+ the last hit when found.
@return empty, when no property is recorded or a list containing property_value and secure_p
@@ -1614,7 +2153,7 @@
cached value if available. If -cache_only is true, will never
incur a database hit (i.e., will only return a value if
cached). If the property is secure, we must be on a validated session
- over HTTPS.
+ over HTTPS or the default is returned.
@param session_id controls which session is used
@param module typically the name of the package to which the property
@@ -1637,7 +2176,7 @@
set id $session_id
}
- set cmd [list sec_lookup_property $id $module $name]
+ set cmd [list sec_lookup_property_not_cached $id $module $name]
if { $cache_only == "t" && ![util_memoize_cached_p $cmd] } {
return $default
@@ -1694,7 +2233,7 @@
}
if { $session_id eq "" } {
- ns_log warning "could not obtain a session_id via 'ad_conn session_id'"
+ ad_log warning "could not obtain a session_id via 'ad_conn session_id'"
} else {
if { $persistent == "t" } {
@@ -1734,17 +2273,62 @@
#
# Perform an upsert operation via stored procedure
#
- db_exec_plsql prop_upsert {}
+ if {[db_driverkey ""] eq "oracle"} {
+ acs::dc call sec_session_property upsert \
+ -p_session_id $session_id \
+ -p_module $module \
+ -p_name $name \
+ -p_value $value \
+ -p_secure $secure \
+ -p_last_hit $last_hit
+ } else {
+ acs::dc call sec_session_property upsert \
+ -session_id $session_id \
+ -module $module \
+ -name $name \
+ -value $value \
+ -secure $secure \
+ -last_hit $last_hit
+ }
}
}
}
# Remember the new value, seeding the memoize cache with the proper value.
- util_memoize_seed [list sec_lookup_property $session_id $module $name] [list $value $secure]
+ util_memoize_seed \
+ [list sec_lookup_property_not_cached $session_id $module $name] \
+ [list $value $secure]
}
+#
+# Provide a global variable for devopers to activate/deactivate
+# client_property_password in case a site has good reasons not to
+# using the client property (e.g. site specific code). This is meant
+# to be transitional code.
+#
+set ::acs::pass_password_as_query_variable 0
+ad_proc -public security::set_client_property_password {password} {
+
+ Convenience function for remembering user password as client property
+ rather than passing it as query parameter.
+
+ @see security::get_client_property_password
+} {
+ ad_set_client_property -persistent f acs-admin user-password $password
+}
+
+ad_proc -public security::get_client_property_password {password} {
+
+ Convenience function for retrieving user password from client property
+
+ @see security::set_client_property_password
+
+} {
+ return [ad_get_client_property acs-admin user-password]
+}
+
#####
#
# security namespace public procs
@@ -1780,7 +2364,7 @@
return [parameter::get \
-boolean \
-parameter RestrictLoginToSSLP \
- -package_id [ad_acs_kernel_id]]
+ -package_id $::acs::kernel_id]
}
ad_proc -public security::require_secure_conn {} {
@@ -1862,7 +2446,7 @@
}
-ad_proc -private security::get_qualified_url { url } {
+ad_proc -public security::get_qualified_url { url } {
@return secure or insecure qualified url
} {
if { [security::secure_conn_p] || [ad_conn behind_secure_proxy_p] } {
@@ -1925,7 +2509,7 @@
return $uri
}
-ad_proc -private security::get_secure_location {} {
+ad_proc -public security::get_secure_location {} {
Return the current location in secure (https) mode.
@author Peter Marklund
@@ -1943,6 +2527,7 @@
# SuppressHttpPort is set.
#
set suppress_http_port [parameter::get -parameter SuppressHttpPort \
+ -boolean \
-package_id [apm_package_id_from_key acs-tcl] \
-default 0]
set secure_location [util::join_location \
@@ -1975,6 +2560,7 @@
# SuppressHttpPort is set.
#
set suppress_http_port [parameter::get -parameter SuppressHttpPort \
+ -boolean \
-package_id [apm_package_id_from_key acs-tcl] \
-default 0]
set insecure_location [util::join_location \
@@ -1990,28 +2576,31 @@
if {[ns_info name] ne "NaviServer"} {
#
- # Compatibility function for AOLserver, which allows abstracts
- # from the configuration section in the config files. NaviServer
- # supports in general global and per-server defined drivers. The
- # emulated version just supports per-server configurations, since
- # these are the only ones supported by AOLserver.
+ # Compatibility function for AOLserver, which abstracts from the
+ # configuration section in the config files. NaviServer supports
+ # in general global and per-server defined drivers.
#
+ # In the emulated version for AOLserver just report the per-server
+ # configurations, since these are the only ones supported by
+ # AOLserver.
+ #
ad_proc -public ns_driversection {
{-driver "nssock"}
{-server ""}
} {
- Return the section name in the config file containing configuration information about the
- network connection.
+ Return the section name in the config file containing
+ configuration information about the network connection.
+
@param driver (e.g. nssock)
- @param server symobolic server name
+ @param server symbolic server name
@return name of section of the drive in the config file
} {
if {$server eq ""} {set server [ns_info server]}
return "ns/server/$server/module/$driver"
}
}
-ad_proc -public ad_server_modules {} {
+ad_proc -private ad_server_modules {} {
Return the list of the available server modules
@author Gustaf Neumann
} {
@@ -2048,43 +2637,69 @@
return $::acs::sdriver
}
-if {[info commands ns_driver] ne ""} {
+if {[namespace which ns_driver] ne ""} {
- ad_proc -private security::configured_driver_info {} {
+ ad_proc -public security::configured_driver_info {} {
Return a list of dicts containing type, driver, location and port
of all configured drivers
@see util_driver_info
} {
- set defaultport {http 80 https 433}
+ set protos {http 80 https 443}
set result {}
foreach i [ns_driver info] {
set type [dict get $i type]
set location [dict get $i location]
set proto [dict get $i protocol]
- set li [ns_parseurl $location]
+ if {$location ne ""} {
+ set li [ns_parseurl $location]
- if {[dict exists $li port]} {
- set port [dict get $li port]
- set suffix ":$port"
+ if {[dict exists $li port]} {
+ set port [dict get $li port]
+ set suffix ":$port"
+ } else {
+ set port [dict get $protos $proto]
+ set suffix ""
+ }
} else {
- set port [dict get $defaultport $proto]
- set suffix ""
+ #
+ # In case we have no "location" defined (e.g. virtual
+ # hosting), get "port" and suffix directly from the
+ # driver.
+ #
+ if {[dict exists $i port]} {
+ set port [lindex [dict get $i port] 0]
+ set defaultport [dict get $i defaultport]
+ } else {
+ set driver_section [ns_driversection -driver [dict exists $i module]]
+ set port [ns_config -int $driver_section port]
+ set defaultport [dict get $protos $proto]
+ }
+ #
+ # Newer versions of NaviServer support multiple ports
+ # per driver. For now, take the first one (similar with "address" below).
+ #
+ set port [lindex [dict get $i port] 0]
+ if {$port eq $defaultport} {
+ set suffix ""
+ } else {
+ set suffix ":$port"
+ }
}
lappend result [list \
proto $proto \
driver [dict get $i module] \
- host [dict get $li host] \
+ host [lindex [dict get $i address] 0] \
location $location port $port suffix $suffix]
}
return $result
}
} else {
- ad_proc -private security::configured_driver_info {} {
+ ad_proc -public security::configured_driver_info {} {
set result ""
#
# Find the first insecure driver based on driver names from
@@ -2174,95 +2789,108 @@
}
}
-ad_proc -public security::locations {} {
+ad_proc -private security::configured_locations {
+ {-suppress_http_port:boolean false}
+ {-secure_conn:boolean false}
+} {
- This function returns the configured locations and the current
- location and the vhost locations, potentially in HTTP or in HTTPs
- variants.
+ This function returns the configured locations.
When the package parameter "SuppressHttpPort" of acs-tcl parameter
is true, then an alternate location without a port is included.
This proc also assumes hostnames from host_node_map table are
accurate and legit.
- The term location refers to protocol://domain:port for
- website.
+ The term location refers to "protocol://domain:port" for website.
- @return insecure location and secure location followed possibly by alternate location(s) as a list.
+ @return list of locations
} {
set locations [list]
set portless_locations {}
#
- # Get Information from configured servers
+ # Get configuration information from the configured servers.
#
set driver_info [security::configured_driver_info]
foreach d $driver_info {
#
- # port == 0 means that the driver is just used for sending, but not for receiveing
+ # port == 0 means that the driver is just used for sending,
+ # but not for receiving. In this case, this entry is not
+ # regarded as a valid location.
#
if {[dict get $d port] != 0} {
+ #
+ # Add configured locations (deprecated, since this
+ # conflicts with the concept of virtual servers).
+ #
set location [dict get $d location]
- if {$location ni $locations} {lappend locations $location}
-
- set location [dict get $d proto]://[dict get $d host]
- if {$location ni $portless_locations &&
- $location ni $locations} {
- lappend portless_locations $location
+ if {$location ne "" && $location ni $locations} {
+ lappend locations $location
}
- append location :[dict get $d port]
- if {$location ni $locations} {lappend locations $location}
- }
- }
- if {[ns_conn isconnected]} {
- #
- # Is the current connection secure?
- #
- set secure_conn_p [expr {[security::secure_conn_p] || [ad_conn behind_secure_proxy_p]}]
-
- set current_location [util_current_location]
- if {$current_location ni $locations} {
- lappend locations $current_location
- }
-
- #
- # When we are on a secure connection, the command above added
- # already a secure connection. When we are on a nonsecure
- # connection, but HTTPS is available, allow as well the
- # current host via the secure connection.
- #
- if {!$secure_conn_p && [https_available_p]} {
- set secure_current_location [security::get_secure_location]
- #ns_log notice "ADD secure_current_location: <$secure_current_location>"
- if {$secure_current_location ni $locations} {
- lappend locations $secure_current_location
+ set hosts [dict get $d host]
+ if {[acs::icanuse "ns_set values"]} {
+ set virtualservers \
+ [ns_configsection ns/module/[dict get $d driver]/servers]
+ if {$virtualservers ne ""} {
+ lappend hosts {*}[ns_set values $virtualservers]
+ }
}
+ foreach entry $hosts {
+ #
+ # The value of the "DRIVER/servers" section might
+ # contain also a port.
+ #
+ set d1 [dict merge $d [ns_parsehostport $entry]]
+ set proto [dict get $d proto]
+ set host [dict get $d1 host]
+ set port [dict get $d1 port]
+ if {$host in {0.0.0.0 ::}} {
+ #
+ # Don't add INADDR_ANY to locations
+ #
+ continue
+ }
+ #
+ # Add always a variant with the omitted default port.
+ #
+ if {($proto eq "https" && $port eq "443")
+ || ($proto eq "http" && $port eq "80")
+ } {
+ set location [util::join_location -proto $proto -hostname $host]
+ if {$location ni $locations} {
+ lappend locations $location
+ }
+ }
+ #
+ # Add a variant with the omitted port to
+ # portless_locations.
+ #
+ set location [util::join_location -proto $proto -hostname $host]
+ if {$location ni $portless_locations
+ && $location ni $locations
+ } {
+ lappend portless_locations $location
+ }
+ #
+ # Add always a variant with the port to locations.
+ #
+ set location [util::join_location -proto $proto -hostname $host -port $port]
+ if {$location ni $locations} {
+ lappend locations $location
+ }
+ }
}
- } else {
- set secure_conn_p 0
}
#
- # Consider if we are behind a proxy and don't want to publish the
- # proxy's backend port. In this cases, SuppressHttpPort can be used
- #
- set suppress_http_port [parameter::get -parameter SuppressHttpPort \
- -package_id [apm_package_id_from_key acs-tcl] \
- -default 0]
- if {$suppress_http_port} {
- lappend locations {*}$portless_locations
- }
-
-
- #
# Add locations from host_node_map
#
- set host_node_map_hosts_list [db_list -cache_key security-locations-host-names \
- get_node_host_names {select host from host_node_map}]
+ set host_node_map_hosts_list \
+ [db_list get_node_host_names {select host from host_node_map}]
if { [llength $host_node_map_hosts_list] > 0 } {
- if { $suppress_http_port } {
+ if { $suppress_http_port_p } {
foreach hostname $host_node_map_hosts_list {
lappend locations "http://${hostname}"
if {$secure_conn_p} {
@@ -2286,6 +2914,103 @@
}
}
}
+
+ if {$suppress_http_port_p} {
+ lappend locations {*}$portless_locations
+ }
+
+ return $locations
+}
+
+ad_proc -public security::locations {} {
+
+ This function returns the configured locations and the current
+ location and the vhost locations, potentially in HTTP or in HTTPs
+ variants.
+
+ When the package parameter "SuppressHttpPort" of acs-tcl parameter
+ is true, then an alternate location without a port is included.
+ This proc also assumes hostnames from host_node_map table are
+ accurate and legit.
+
+ The term location refers to protocol://domain:port for
+ website.
+
+ @return insecure location and secure location followed possibly by alternate location(s) as a list.
+
+} {
+ #
+ # Is the current connection secure?
+ #
+ set secure_conn_p [expr {[ns_conn isconnected]
+ ? ([security::secure_conn_p] || [ad_conn behind_secure_proxy_p])
+ : 0}]
+ #
+ # Consider if we are behind a proxy and don't want to publish the
+ # proxy's backend port. In this cases, SuppressHttpPort can be used
+ #
+ set suppress_http_port_p [parameter::get -parameter SuppressHttpPort \
+ -boolean \
+ -package_id [apm_package_id_from_key acs-tcl] \
+ -default 0]
+ #
+ # Get Information from configured servers
+ #
+ set locations [acs::misc_cache eval security-configure-locations-$suppress_http_port_p-$secure_conn_p {
+ set locations [security::configured_locations -suppress_http_port=$suppress_http_port_p -secure_conn=$secure_conn_p]
+ #
+ # The configured values values do not change at runtime. Set
+ # it also once in the nsv array when setting the cache value.
+ #
+ foreach location $locations {
+ nsv_set validated_location $location 1
+ }
+ set locations
+ }]
+
+ #
+ # Add the previously validated locations
+ #
+ foreach location [nsv_array names validated_location] {
+ if {$location ni $locations} {
+ lappend locations $location
+ }
+ }
+
+
+ #
+ # When we are connected, add the current location if is not there
+ # already, also potentially in a secure fashion.
+ #
+ # This is probably not needed, but is kept here for backwards
+ # compatibility. For the time being, add log statements when this
+ # happens.
+ #
+ if {[ns_conn isconnected]} {
+
+ set current_location [util_current_location]
+ if {$current_location ni $locations} {
+ ns_log notice "security::locations add connected location <$current_location>"
+ lappend locations $current_location
+ nsv_set validated_location $current_location 1
+ }
+
+ #
+ # When we are on a secure connection, the command above added
+ # already a secure connection. When we are on a nonsecure
+ # connection, but HTTPS is available, allow as well the
+ # current host via the secure connection.
+ #
+ if {!$secure_conn_p && [https_available_p]} {
+ set secure_current_location [security::get_secure_location]
+ if {$secure_current_location ni $locations} {
+ ns_log notice "security::locations add connected secure location <$secure_current_location>"
+ lappend locations $secure_current_location
+ nsv_set validated_location $secure_current_location 1
+ }
+ }
+ }
+
#ns_log notice "security::locations <$locations>"
return $locations
}
@@ -2296,21 +3021,76 @@
@param host host from host header field.
} {
#
- # The global variable takes care of outputting error message only
+ # The per-request cache takes care of outputting error message only
# once per request.
#
- set key ::__security_provided_host_validated($host)
- if {![info exists $key]} {
- set $key 1
+ return [acs::per_request_cache eval -key acs-tcl.security_provided_host_validated-$host {
+ set result 1
if {$host ne ""} {
if {![regexp {^[\w.:@+/=$%!*~\[\]-]+$} $host]} {
+ #
+ # Don't use "ad_log", since this might leed to a recursive loop.
+ #
binary scan [encoding convertto utf-8 $host] H* hex
- ad_log warning "provided host <$host> (hex $hex) contains invalid characters"
- set $key 0
+ ns_log warning "provided host <$host> (hex $hex) contains invalid characters\n\
+ URL: [ns_conn url]\npeer addr:[ad_conn peeraddr]"
+ set result 0
}
}
+ set result
+ }]
+}
+
+ad_proc security::secure_hostname_p {host} {
+
+ Check, if the content of host is a "secure" value, which means, it
+ is either white-listed or belongs to a non-public IP address,
+ such it cannot harm in redirect operations.
+
+ @return boolean value
+} {
+ #
+ # If the host has an non-public IP address (such as
+ # e.g. "localhost") it is regarded as "secure". The first test is
+ # the most simple case, working for all versions of NaviServer or
+ # AOLserver.
+ #
+ if {$host in {localhost 127.0.0.1 ::1}} {
+ return 1
}
- return [set $key]
+
+ set validationOk 0
+ if {[acs::icanuse "ns_ip"]} {
+ #
+ # Check, if the address is not public. It resolves the
+ # $hostName and checks the properties of the first IP address
+ # returned.
+ #
+ set validationOk [expr {![ns_ip public [ns_addrbyhost $host]]}]
+
+ } elseif {[acs::icanuse "ns_subnetmatch"]} {
+ #
+ # Test for older versions of NaviServer testing if value is an
+ # IP address belonging to a "private network".
+ #
+ try {
+ ns_subnetmatch 0.0.0.0/0 $host
+ } on error {errorMsg} {
+ set ip_address_p 0
+ } on ok {ip_address_p} {
+ }
+ if {$ip_address_p} {
+ if {[ns_subnetmatch 10.0.0.0/8 $host]
+ || [ns_subnetmatch 172.16.0.0/12 $host]
+ || [ns_subnetmatch 192.168.0.0/16 $host]
+ || [ns_subnetmatch fd00::/8 $host]
+ } {
+ return 1
+ }
+ }
+ }
+
+ return 0
}
ad_proc -public security::validated_host_header {} {
@@ -2321,85 +3101,67 @@
attacks can lead to web-cache poisoning and password reset attacks
(for more details, see e.g.
http://www.skeletonscribe.net/2013/05/practical-http-host-header-attacks.html)
+ or to unintended redirects to different sites.
+
+ The validated host header most be syntactically correct, and it
+ must be either configured/white-listed or it must be from a
+ non-routable IP address. White-listed hosts are taken from the
+ alternate host names specified in the "ns/module/DRIVER/servers"
+ section, or via the configuration variable "hostname" (e.g.,
+ "openacs.org www.openacs.org") which is added the the "/server"
+ section during startup.
+
} {
#
# Check, if we have a host header field
#
- set host [ns_set iget [ns_conn headers] Host]
- if {$host eq ""} {
+ set hostHeaderValue [ns_set iget [ns_conn headers] Host]
+ if {$hostHeaderValue eq ""} {
return ""
}
+ #
+ # Domain names are case insensitive. So convert it to lower to
+ # avoid surprises.
+ #
+ set hostHeaderValue [string tolower $hostHeaderValue]
#
# Check, if we have validated it before, or it belongs to the
# predefined accepted host header fields.
#
- set key ::acs::validated($host)
+ set key ::acs::validated_host_header($hostHeaderValue)
if {[info exists $key]} {
- return $host
+ return $hostHeaderValue
}
- if {![string match *//* $host]} {
- set splithost [ns_conn protocol]://$host
- } else {
- set splithost $host
- }
- if {![util::split_location $splithost .proto hostName hostPort]} {
- return ""
- }
-
+ set hostHeaderDict [ns_parsehostport $hostHeaderValue]
#
# Remove trailing dot, as this is allowed in fully qualified DNS
# names (see e.g. ยง3.2.2 of RFC 3976).
#
- set hostName [string trimright $hostName .]
+ set hostName [string trimright [dict get $hostHeaderDict host] .]
+ set hostPort [expr {[dict exists $hostHeaderDict port] ? [dict get $hostHeaderDict port] : ""}]
- #
- # Check, if the provided host is the same as the configured host
- # name for the current driver or one of its IP addresses. Should
- # be true in most cases.
- #
- set driverInfo [util_driver_info]
- set driverHostName [dict get $driverInfo hostname]
- if {$hostName eq $driverHostName || $hostName in [ns_addrbyhost -all $driverHostName]} {
- #
- # port is currently ignored
- #
- set $key 1
- return $host
- }
+ set normalizedHostHeaderValue [util::join_location -host $hostName -port $hostPort]
+ set validationOk 0
#
- # Check, if the provided host is the same in [ns_conn location]
- # (will be used as default, but we do not want a warning in such
- # cases).
+ # Check if the value in "hostName" can be regarded as safe.
#
- if {[util::split_location [ns_conn location] proto locationHost locationPort]} {
- if {$hostName eq $locationHost} {
+ # The host header value is one of the names registered for
+ # this server.
+ #
+ if {[acs::icanuse "ns_server hosts"]} {
+ if {$normalizedHostHeaderValue in [ns_server hosts]} {
#
- # port is currently ignored
#
- set $key 1
- return $host
+ set validationOk 1
}
- }
-
- #
- # Check, if the provided host is the same as in the configured
- # SystemURL.
- #
- if {[util::split_location [ad_url] .proto systemHost systemPort]} {
- if {$hostName eq $systemHost
- && ($hostPort eq $systemPort || $hostPort eq "") } {
- set $key 1
- return $host
- }
- }
-
- #
- # Check against the virtual server configuration of NaviServer.
- #
- if {[ns_info name] eq "NaviServer"} {
+ } elseif {[ns_info name] eq "NaviServer"} {
+ #
+ # As a replacement for "ns_server hosts" check against the
+ # virtual server configuration of NaviServer.
+ #
set s [ns_info server]
set driverInfo [security::configured_driver_info]
set drivers [lmap d $driverInfo {dict get $d driver}]
@@ -2417,60 +3179,118 @@
if {$key ne $s} continue
set value
}]
- if {$host in $names} {
- ns_log notice "security::validated_host_header: found $host via global virtual server configuration for $driver"
- set $key 1
- return $host
+ if {$normalizedHostHeaderValue in $names} {
+ ns_log notice "security::validated_host_header: found $hostHeaderValue" \
+ "in global virtual server configuration for $driver"
+ return 1
}
}
}
}
- #
- # Check against host node map. Here we need as well protection
- # against invalid utf-8 characters.
- #
- if {![security::provided_host_valid $hostName]} {
- return ""
+ if {$validationOk == 0} {
+ set validationOk [security::secure_hostname_p $hostName]
}
- set result [db_list host_header_field_mapped {select 1 from host_node_map where host = :hostName}]
- #ns_log notice "security::validated_host_header: checking entry <$hostName> from host_node_map -> $result"
- if {$result == 1} {
+ if {$validationOk == 0} {
#
- # port is ignored
+ # Check against the white-listed hosts from
#
- set $key 1
- #ns_log notice "security::validated_host_header: checking entry <$hostName> from host_node_map return host <$host>"
- return $host
+ # ns_section ns/server/$server/acs {
+ # ns_param whitelistedHosts {...}
+ # }
+ #
+ # of the configuration file.
+ #
+ if {$hostHeaderValue in [ns_config "ns/server/[ns_info server]/acs" whitelistedHosts {}]} {
+ set validationOk 1
+ }
}
- #
- # Handle aliases for locations, which cannot be determined from
- # config files, but which are supposed to be ok.
- #
- if {$hostName eq "localhost"} {
+ if {$validationOk == 0} {
#
- # This is not an attempt, where someone tries to lure us to a
- # different host via redirect.
+ # Check against host node map. Here we need as well protection
+ # against invalid utf-8 characters.
#
- set $key 1
- return $host
+ if {![security::provided_host_valid $hostName]} {
+ return ""
+ }
+
+ set validationOk [db_0or1row host_header_field_mapped {
+ select 1 from host_node_map where host = :hostName
+ }]
}
+ if {$validationOk == 0} {
+ #
+ # Validation is OK, when the hostName is either the same as
+ # configured hostname. This is a legacy branch for very old
+ # versions of NaviServer or AOLserver.
+ #
+ set driverInfo [util_driver_info]
+ set driverHostName [dict get $driverInfo hostname]
+ if {$hostName eq $driverHostName} {
+ set validationOk 1
+ }
+ }
+ if {$validationOk == 0 && [info exists driverHostName]} {
+ #
+ # Validation is OK, when the hostName is one of the IP
+ # addresses of the configured host name.
+ #
+ try {
+ ns_addrbyhost -all $driverHostName
+ } on error {errorMsg} {
+ #
+ # Name resolution of the hostname configured for this
+ # driver failed, we cannot validate incoming IP addresses.
+ #
+ ns_log error "security::validated_host_header: configuration error:" \
+ "name resolution for configured hostname '$driverHostName'" \
+ "of driver '[ad_conn driver]' failed"
+ } on ok {result} {
+ set validationOk [expr {$hostName in $result}]
+ }
+ }
+
#
- # We could/should check as well against a white-list of additional
- # host names (maybe via ::acs::validated, or via config file, or
- # via additional package parameter). Probably the best way is to
- # get alternate (alias) names from the driver section of the
- # current driver [ns_conn driver] (maybe check global and local).
+ # Check, if the provided host is the same in [ns_conn location]
+ # (will be used as default, but we do not want a warning in such
+ # cases). This is also a legacy case.
#
- #ns_set array [ns_configsection ns/module/nssock/servers]
+ if {$validationOk == 0
+ && [util::split_location [ns_conn location] proto locationHost locationPort]} {
+ set validationOk [expr {$hostName eq $locationHost}]
+ }
#
+ # Check, if the provided host is the same as in the configured
+ # SystemURL. Legacy case.
+ #
+ if {$validationOk == 0 && [util::split_location [ad_url] .proto systemHost systemPort]} {
+ set validationOk [expr {$hostName eq $systemHost
+ && ($hostPort eq $systemPort || $hostPort eq "") }]
+ }
+
+
+ #
+ # When any of the validation attempts above were successful, we
+ # are done. We keep the logic for successful lookups
+ # centralized. Performance of the individual tests are not
+ # critical, since the lookups are cache per thread.
+ #
+ if {$validationOk} {
+ set $key 1
+ return $hostHeaderValue
+ }
+
+
+ #
# Now we give up
#
- ns_log warning "ignore untrusted host header field: '$host'"
+ ns_log warning "ignore untrusted host header field: '$hostHeaderValue'." \
+ "Consider adding this value to 'whitelistedHosts' in the" \
+ "section 'ns/server/\$server/acs' of your configuration file"
return ""
}
@@ -2514,9 +3334,9 @@
#
set session_id [ad_conn peeraddr]
}
- set secret [ns_config "ns/server/[ns_info server]/acs" parametersecret ""]
+ set secret [ns_config "ns/server/[ns_info server]/acs" parameterSecret ""]
- if {[info commands ::crypto::hmac] ne ""} {
+ if {[namespace which ::crypto::hmac] ne ""} {
set token [::crypto::hmac string $secret $session_id-[clock clicks -microseconds]]
} else {
set token [ns_sha1 "$secret-$session_id-[clock clicks -microseconds]"]
@@ -2544,9 +3364,9 @@
lappend $var $value
}
if {$force_p} {
- ns_log notice "CSP: forcing $directive $value"
set var ::__csp__directive_forced($directive)
if {![info exists $var] || $value ni [set $var]} {
+ ns_log notice "CSP: forcing $directive $value"
lappend $var $value
}
}
@@ -2571,14 +3391,15 @@
#
security::csp::require default-src 'self'
security::csp::require script-src 'self'
+ #security::csp::require script-src 'strict-dynamic'
security::csp::require style-src 'self'
security::csp::require img-src 'self'
security::csp::require font-src 'self'
security::csp::require base-uri 'self'
-
+ security::csp::require connect-src 'self'
#
# Some browser (safari, chrome) need "font-src data:", maybe
- # for plugins or diffent font settings. Seems safe enough.
+ # for plugins or different font settings. Seems safe enough.
#
security::csp::require font-src data:
@@ -2596,7 +3417,7 @@
# Another problem is mixed content. When we set the nonce-src
# and 'unsafe-inline', and a browser honoring nonces ignores
# the 'unsafe-inline', but some JavaScript framework requires
- # it (e.g ckeditor4), we have a problem. Therefore, an
+ # it (e.g. ckeditor4), we have a problem. Therefore, an
# application can force "'unsafe-inline'" which means that we
# do not set the nonce-src in such cases.
#
@@ -2613,34 +3434,45 @@
security::csp::require style-src 'unsafe-inline'
#
- # Define a report URI to ease debugging. CSP 3 will support a
- # "report-to" directive, but will still support "report-uri".
+ # Use newer "report-to" will be preferred and "report-uri"
+ # deprecated. As of May 2020: no support for "report-to" for
+ # FF (75, or forthcoming 66 and 77) or Safari.
+ # https://caniuse.com/#search=report-to
#
security::csp::require report-uri /SYSTEM/csp-collector.tcl
+ #ns_set [ns_conn outputheaders] Report-To "{'url':'/SYSTEM/csp-collector.tcl','group':'csp-endpoint','max-age':10886400}"
+ #security::csp::require report-to csp-endpoint
#
# We do not need object-src
#
security::csp::require object-src 'none'
+ security::csp::require form-action 'self'
+ security::csp::require frame-ancestors 'self'
+
+ #security::csp::require require-trusted-types-for 'script'
+
set policy ""
foreach directive {
+ base-uri
child-src
connect-src
default-src
font-src
form-action
- frame-src
frame-ancestors
+ frame-src
img-src
media-src
object-src
plugin-types
report-uri
+ require-trusted-types-for
sandbox
script-src
style-src
- base-uri
+ trusted-types
} {
set var ::__csp__directive($directive)
if {[info exists $var]} {
@@ -2650,8 +3482,67 @@
return $policy
}
+ ad_proc -public ::security::csp::add_static_resource_header {
+ {-mime_type:required}
+ } {
+
+ Set the CSP rule on the current connection for a static
+ resource depending on the MIME type.
+
+ @param mime_type MIME type of the resource to be delivered
+ } {
+ if {![ns_conn isconnected]} {
+ error "Content-Security-Policy headers can be only set for active connections"
+ }
+ if {[dict exists $::security::csp::static_csp $mime_type]} {
+ ns_set iupdate [ns_conn outputheaders] \
+ "Content-Security-Policy" [dict get $::security::csp::static_csp $mime_type]
+ ns_log notice "STATIC $mime_type: Content-Security-Policy [dict get $::security::csp::static_csp $mime_type]"
+ } else {
+ #ns_log notice "STATIC $mime_type: no Content-Security-Policy defined for this MIME type"
+ }
+ }
}
+namespace eval ::security::parameter {
+
+ ad_proc -public signed {{-max_age ""} value} {
+
+ Compute a compact single-token signed value based on the
+ parameterSecret.
+
+ @see ::security::parameter::validated
+ } {
+ set token_id [sec_get_random_cached_token_id]
+ set secret [ns_config "ns/server/[ns_info server]/acs" parameterSecret ""]
+ set signature [ad_sign -max_age $max_age -secret $secret -token_id $token_id $value]
+ return [ns_base64urlencode [list $value $signature]]
+ }
+
+ ad_proc -public validated {input} {
+
+ Validate the single-token signed value and return its content value.
+ Raise an exception, when the signature is broken.
+
+ @see ::security::parameter::signed
+
+ } {
+ set success 0
+ set pair [ns_base64urldecode $input]
+ if {[string is list -strict $pair] && [llength $pair] == 2} {
+ lassign $pair value signature
+ set secret [ns_config "ns/server/[ns_info server]/acs" parameterSecret ""]
+ set success [ad_verify_signature -secret $secret $value $signature]
+ }
+ if {$success} {
+ return $value
+ } else {
+ ad_raise invalid_signature
+ }
+ }
+}
+
+
#TODO remove me: just for a transition phase
proc ::security::nonce_token args {uplevel ::security::csp::nonce {*}$args}
@@ -2666,21 +3557,27 @@
# security::csrf::new
# security::csrf::validate
- ad_proc -public ::security::csrf::new {{-tokenname __csrf_token} -user_id} {
+ ad_proc -public ::security::csrf::new {
+ {-tokenname __csrf_token}
+ -user_id
+ } {
Create a security token to protect against CSRF (Cross-Site
Request Forgery). The token is set (and cached) in a global
- per-thread variable an can be included in forms e.g. via the
+ per-thread variable and can be included in forms e.g. via the
following command.
-
-
+
+ <if @::__csrf_token@ defined> + <input type="hidden" name="__csrf_token" value="@::__csrf_token;literal@"> + </if> +
The token is automatically cleared together with other global variables at the end of the processing of every request. - +
The optional argument user_id is currently ignored, but it is there, since there are algorithms published to calculate the - CSRF token based on an user_id. So far, i found no evidence + CSRF token based on a user_id. So far, i found no evidence that these should be used, but the argument is there as a reminder, such the interface does not have to be used, when we switch to such an algorithm. @@ -2701,10 +3598,13 @@ # # validate # - ad_proc -public ::security::csrf::validate {{-tokenname __csrf_token} {-allowempty false}} { + ad_proc -public ::security::csrf::validate { + {-tokenname __csrf_token} + {-allowempty false} + } { - Validate a CSRF token and call security::csrf::fail the request if - invalid. + Validate a CSRF token and call security::csrf::fail the + request if invalid. @return nothing } { @@ -2721,7 +3621,7 @@ set oldToken [ns_queryget $tokenname] if {$oldToken eq ""} { # - # There is not token in the query/form parameters, we + # There is no token in the query/form parameters, we # can't validate, since there is no token. # if {$allowempty} { @@ -2757,7 +3657,7 @@ set session_id [ad_conn peeraddr] } else { # - # User is logged in, use a session token. + # User is logged-in, use a session token. # set session_id [ad_conn session_id] } @@ -2767,7 +3667,9 @@ # # Generate CSRF token # - ad_proc -private ::security::csrf::token { {-tokenname __csrf_token} } { + ad_proc -private ::security::csrf::token { + {-tokenname __csrf_token} + } { Generate a CSRF token and return it @@ -2783,8 +3685,8 @@ if {[info exists $globalTokenName] && [set $globalTokenName] ne ""} { set token [set $globalTokenName] } else { - set secret [ns_config "ns/server/[ns_info server]/acs" parametersecret ""] - if {[info commands ::crypto::hmac] ne ""} { + set secret [ns_config "ns/server/[ns_info server]/acs" parameterSecret ""] + if {[namespace which ::crypto::hmac] ne ""} { set token [::crypto::hmac string $secret [session_id]] } else { set token [ns_sha1 $secret-[session_id]] @@ -2814,6 +3716,7 @@ } } +nsv_set validated_location http://localhost 1 # # Local variables: # mode: tcl