diff options
| author | Michael Peter Christen <mc@yacy.net> | 2026-07-11 20:52:05 +0200 |
|---|---|---|
| committer | Michael Peter Christen <mc@yacy.net> | 2026-07-11 20:52:05 +0200 |
| commit | edeb6162c435e4ad9098233eed4f8c9c9b0312e2 (patch) | |
| tree | 4b09aecccf8a4f39fcd0c3c8c1bad6e14d091348 /source | |
| parent | dd0d4ee962dfc50105c0465b8ea0dc2310e601d2 (diff) | |
de-coupling of security functions from jetty
Diffstat (limited to 'source')
| -rw-r--r-- | source/net/yacy/cora/protocol/RequestHeader.java | 38 | ||||
| -rw-r--r-- | source/net/yacy/http/AdminSecurity.java | 139 | ||||
| -rw-r--r-- | source/net/yacy/http/YaCyDigestCredential.java | 140 | ||||
| -rw-r--r-- | source/net/yacy/http/YaCyLegacyCredential.java | 128 | ||||
| -rw-r--r-- | source/net/yacy/http/YaCyLoginService.java | 2 | ||||
| -rw-r--r-- | source/net/yacy/http/YaCySecurityHandler.java | 115 | ||||
| -rw-r--r-- | source/net/yacy/search/Switchboard.java | 5 | ||||
| -rw-r--r-- | source/net/yacy/server/serverAccessTracker.java | 7 |
8 files changed, 393 insertions, 181 deletions
diff --git a/source/net/yacy/cora/protocol/RequestHeader.java b/source/net/yacy/cora/protocol/RequestHeader.java index 202be51da..2a4140af8 100644 --- a/source/net/yacy/cora/protocol/RequestHeader.java +++ b/source/net/yacy/cora/protocol/RequestHeader.java @@ -139,8 +139,10 @@ public class RequestHeader extends HeaderFramework implements HttpServletRequest } public boolean accessFromLocalhost() { - // authorization for localhost, only if flag is set to grant localhost access as admin - final String clientIP = this.getRemoteAddr(); + // authorization for localhost, only if flag is set to grant localhost access as admin. + // This is an access-control decision, therefore the true socket peer must be used: + // the client-controlled (and trivially spoofable) X-Real-IP header must NOT be honored here. + final String clientIP = this.getRemoteSocketAddr(); if ( !Domains.isLocalhost(clientIP) ) { return false; } @@ -352,6 +354,9 @@ public class RequestHeader extends HeaderFramework implements HttpServletRequest return null; } + // Invariant relied on for authorization: the role/principal come solely from the servlet + // container's authentication. Do not change these to derive from headers/attributes (which + // are client-controllable), else authorization based on them becomes spoofable. @Override public boolean isUserInRole(String role) { if (_request != null) { @@ -696,6 +701,35 @@ public class RequestHeader extends HeaderFramework implements HttpServletRequest return super.get(HeaderFramework.CONNECTION_PROP_CLIENTIP); } + /** + * The IP address of the host that opened the TCP connection (the real socket peer). + * <p> + * In contrast to {@link #getRemoteAddr()} and {@link #client(ServletRequest)} this + * <b>never</b> honors the client-controlled X-Real-IP request header. It must therefore + * be used for all authentication and access-control decisions: X-Real-IP is trivially + * spoofable by a direct client and must only be trusted for peer routing behind a + * trusted reverse proxy, not for authentication. + * + * @return the socket peer IP address + */ + public String getRemoteSocketAddr() { + if (this._request != null) { + return this._request.getRemoteAddr(); + } + return super.get(HeaderFramework.CONNECTION_PROP_CLIENTIP); + } + + /** + * Resolve the client IP for peer routing and logging. This honors the X-Real-IP + * request header (set e.g. by an nginx reverse proxy via + * "proxy_set_header X-Real-IP $remote_addr;"). + * <p> + * <b>Do not use this for authentication or access control</b> - the header is + * client-controlled and spoofable. Use {@link #getRemoteSocketAddr()} for that. + * + * @param request the servlet request + * @return the routing client IP address + */ public static String client(final ServletRequest request) { String clientHost = request.getRemoteAddr(); if (request instanceof HttpServletRequest) { diff --git a/source/net/yacy/http/AdminSecurity.java b/source/net/yacy/http/AdminSecurity.java new file mode 100644 index 000000000..20cffa094 --- /dev/null +++ b/source/net/yacy/http/AdminSecurity.java @@ -0,0 +1,139 @@ +// +// AdminSecurity +// Copyright 2011 by Florian Richter +// First released 16.04.2011 at https://yacy.net +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program in the file lgpl21.txt +// If not, see <http://www.gnu.org/licenses/>. +// + +package net.yacy.http; + +import net.yacy.cora.order.Base64Order; +import net.yacy.cora.order.Digest; +import net.yacy.cora.protocol.Domains; + +/** + * Decision logic of YaCy's built-in administrator account security: which + * paths are protected, when localhost access is granted without login and + * verification of the supported admin password hash formats. + * + * All methods are pure functions on their parameters, free of servlet + * container (Jetty) and Switchboard dependencies: the container specific + * classes YaCySecurityHandler, YaCyLoginService and YaCyDigestCredential are + * thin adapters delegating here (extracted from them to ease servlet + * container migration). + */ +public final class AdminSecurity { + + private AdminSecurity() { + } + + /** + * Decide whether a path may only be accessed with admin rights. + * Pages suffixed with "_p" are always considered protected. When all pages + * are protected, paths used for peer-to-peer or cluster communication stay + * public (e.g. /yacy/hello.html for p2p presence, /solr/select for remote + * Solr searches), except in private robinson mode. + * + * @param pathInContext the request path + * @param adminForAllPages configuration: all pages need admin rights + * @param privateRobinsonMode true when this peer is in non-public robinson mode + * @param publicSearchpage configuration: the search page is public + * @return true when the path needs admin rights + */ + public static boolean isProtectedPath(final String pathInContext, final boolean adminForAllPages, + final boolean privateRobinsonMode, final boolean publicSearchpage) { + boolean protectedPage = adminForAllPages && (privateRobinsonMode || + !(pathInContext.startsWith("/yacy/") || pathInContext.startsWith("/solr/"))); + protectedPage = protectedPage || (pathInContext.indexOf("_p.") > 0); + if (!protectedPage && !publicSearchpage) { + protectedPage = pathInContext.startsWith("/solr/") || pathInContext.startsWith("/gsa/"); + } + return protectedPage; + } + + /** + * @param remoteip ip address of the client. This must be the true socket peer address: + * callers must not pass an X-Real-IP / X-Forwarded-For derived address here, + * as those headers are client-controlled and spoofable. + * @param refererHost host part of the Referer request header (null when absent or unparseable) + * @return true when the request comes from localhost and is not referred from a remote page + */ + public static boolean isLocalhostAccess(final String remoteip, final String refererHost) { + return Domains.isLocalhost(remoteip) + && (refererHost == null || refererHost.isEmpty() || Domains.isLocalhost(refererHost)); + } + + /** + * Lazy authentication for localhost access: accept Basic credentials that + * contain the configured admin password hash as password (only a user with + * read access to DATA can know that hash). + * + * @param authorizationHeader value of the Authorization request header (may be null) + * @param adminUser configured name of the admin user + * @param adminAccountBase64MD5 configured admin password hash + * @return true when the header contains the admin hash as Basic credential + */ + public static boolean checkLocalhostLazyAuth(final String authorizationHeader, final String adminUser, + final String adminAccountBase64MD5) { + // Basic credentials are short "Basic " + b64(user:pwd) + if (authorizationHeader != null && authorizationHeader.length() < 120 && authorizationHeader.startsWith("Basic ")) { + final String b64 = Base64Order.standardCoder.encodeString(adminUser + ":" + adminAccountBase64MD5); + return authorizationHeader.substring(6).equals(b64); + } + return false; + } + + /** + * Verify a clear text password (BASIC auth) against the configured admin + * password hash. Two hash formats are supported: + * "MD5:" + MD5Hex(user:realm:password) and the Base64 based format + * MD5Hex(Base64(user:password)). + * + * For both formats the configured hash itself is also accepted as password + * when the request comes from localhost: this allows the scripts in bin/ + * (based on bin/apicall.sh) to steer a peer without knowing the clear text + * password. + * + * @param foruser user name the credential was created for + * @param configHash the configured password hash + * @param realm the configured authentication realm (part of "MD5:" hashes) + * @param adminUser configured name of the admin user + * @param fromLocalhost true when the request being authenticated comes from localhost + * (determined from the request's true socket peer IP) + * @param pw the clear text password to check + * @return true when the password matches the configured hash + */ + public static boolean checkAdminPassword(final String foruser, final String configHash, final String realm, + final String adminUser, final boolean fromLocalhost, final String pw) { + if (!configHash.startsWith("MD5:")) { // B64MD5 admin hashes without realm + if (fromLocalhost && pw.equals(configHash)) return true; + return calcHash(foruser + ":" + pw).equals(configHash); + } + final boolean success = Digest.encodeMD5Hex(foruser + ":" + realm + ":" + pw).equals(configHash.substring(4)); + if (!success && foruser.equals(adminUser) && fromLocalhost && pw.equals(configHash)) return true; + return success; + } + + /** + * internal hash function for the Base64 based admin account hash format + * + * @param pw clear password + * @return hash string MD5Hex(Base64(pw)) + */ + public static String calcHash(final String pw) { + return Digest.encodeMD5Hex(Base64Order.standardCoder.encodeString(pw)); + } +} diff --git a/source/net/yacy/http/YaCyDigestCredential.java b/source/net/yacy/http/YaCyDigestCredential.java new file mode 100644 index 000000000..450c12f1f --- /dev/null +++ b/source/net/yacy/http/YaCyDigestCredential.java @@ -0,0 +1,140 @@ +// +// YaCyDigestCredential +// Copyright 2011 by Florian Richter +// First released 16.04.2011 at https://yacy.net +// +// $LastChangedDate$ +// $LastChangedRevision$ +// $LastChangedBy$ +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program in the file lgpl21.txt +// If not, see <http://www.gnu.org/licenses/>. +// + +package net.yacy.http; + +import net.yacy.cora.protocol.Domains; +import net.yacy.search.Switchboard; +import net.yacy.search.SwitchboardConstants; + +import org.eclipse.jetty.util.security.Credential; + + + +/** + * implementation of YaCy's admin password as jetty Credential + * supporting BASIC and DIGEST authentication + * and using MD5 digested passwords/credentials. Following RFC recommendation (to use the realm in MD5 hash) + * expecting a MD5 hash in format MD5( username:realm:password ), realm configured in yacy.init adminRealm + * (a credential in format MD5( username:password ) is also accepted with BASIC auth) + * + * This is a thin adapter to the servlet container: the password verification + * logic is in the container neutral {@link AdminSecurity}. + */ +public class YaCyDigestCredential extends Credential { + + private static final long serialVersionUID = -3527894085562480001L; + + /** + * True socket peer IP of the request currently being authenticated on this thread. + * <p> + * Jetty's {@link Credential#check(Object)} API does not hand over the request, so the + * credential can not tell on its own whether a request comes from localhost. The + * {@link YaCySecurityHandler} therefore publishes the request's socket peer IP here for + * the duration of the request (set at the start of handling, cleared in a finally block). + * This replaces the former global "recent localhost access" timestamp, which was a + * process-wide value not bound to the request being checked. + */ + private static final ThreadLocal<String> REQUEST_CLIENT_IP = new ThreadLocal<String>(); + + /** + * Publish the socket peer IP of the request being authenticated on the current thread. + * Must be paired with {@link #clearRequestClientIP()} in a finally block. + * @param ip the true socket peer IP (never an X-Real-IP derived address) + */ + public static void setRequestClientIP(final String ip) { + REQUEST_CLIENT_IP.set(ip); + } + + /** + * Remove the request client IP published for the current thread. + */ + public static void clearRequestClientIP() { + REQUEST_CLIENT_IP.remove(); + } + + /** + * @return true when the request currently authenticated on this thread comes from + * localhost. Fails closed (returns false) when no request IP was published. + */ + private static boolean isRequestFromLocalhost() { + final String ip = REQUEST_CLIENT_IP.get(); + return ip != null && Domains.isLocalhost(ip); + } + + private String hash; // remember password hash, either MD5(Base64(user:pwd)) or with encryption prefix "MD5:" + MD5(user:realm:pwd) + private String foruser; // remember the user as YaCy credential is username:pwd (not just pwd) + private Credential c; + + @Override + public boolean check(Object credentials) { + + if (credentials instanceof Credential) { // for DIGEST auth + if (this.c == null) { + /* credential may be null after switching from BASIC to DIGEST authentication without re-encoding the password */ + return false; + } + Credential credential = (Credential) credentials; + return credential.check(this.c); + } + if (credentials instanceof String) { // for BASIC auth + final Switchboard sb = Switchboard.getSwitchboard(); + // The "fromLocalhost" exception (see AdminSecurity.checkAdminPassword) lets a + // localhost caller submit the stored password hash itself as the password. This + // is what the bin/*.sh scripts do via bin/apicall.sh: they read the hash from the + // configuration file to steer a local peer, because the cleartext password is + // never stored anywhere. + // + // We must therefore know whether THIS request comes from localhost. Jetty's + // Credential.check() is not given the request, so YaCySecurityHandler publishes + // the request's true socket peer IP into REQUEST_CLIENT_IP for the duration of the + // request (see the detailed rationale on YaCySecurityHandler.handle()); we read it + // back here. Using the socket peer - not the spoofable X-Real-IP header - keeps + // this exception restricted to genuine local callers. + return AdminSecurity.checkAdminPassword(this.foruser, this.hash, + sb.getConfig(SwitchboardConstants.ADMIN_REALM, ""), + sb.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME, "admin"), + isRequestFromLocalhost(), + (String) credentials); + } + throw new UnsupportedOperationException(); + } + + /** + * create Credential object from config file hash + * + * @param configHash hash as in config file hash(adminuser:pwd) + * @return + */ + public static Credential getCredentialForAdmin(String username, String configHash) { + YaCyDigestCredential yc = new YaCyDigestCredential(); + if (configHash.startsWith("MD5:")) { + yc.c = Credential.getCredential(configHash); // for DIGEST auth + } + yc.foruser = username; + yc.hash = configHash; + return yc; + } + +} diff --git a/source/net/yacy/http/YaCyLegacyCredential.java b/source/net/yacy/http/YaCyLegacyCredential.java deleted file mode 100644 index 961e5c529..000000000 --- a/source/net/yacy/http/YaCyLegacyCredential.java +++ /dev/null @@ -1,128 +0,0 @@ -// -// YaCyLegacyCredentials -// Copyright 2011 by Florian Richter -// First released 16.04.2011 at https://yacy.net -// -// $LastChangedDate$ -// $LastChangedRevision$ -// $LastChangedBy$ -// -// This library is free software; you can redistribute it and/or -// modify it under the terms of the GNU Lesser General Public -// License as published by the Free Software Foundation; either -// version 2.1 of the License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with this program in the file lgpl21.txt -// If not, see <http://www.gnu.org/licenses/>. -// - -package net.yacy.http; - -import net.yacy.cora.order.Base64Order; -import net.yacy.cora.order.Digest; -import net.yacy.search.Switchboard; -import net.yacy.search.SwitchboardConstants; -import net.yacy.server.serverAccessTracker; - -import org.eclipse.jetty.util.security.Credential; - - - -/** - * implementation of YaCy's old admin password as jetty Credential - * supporting BASIC and DIGEST authentication - * and using MD5 encryptet passwords/credentials. Following RFC recommendation (to use the realm in MD5 hash) - * expecting a MD5 hash in format MD5( username:realm:password ), realm configured in yacy.init adminRealm - * (exception: old style credential MD5( username:password ) still accepted with BASIC auth) - * - */ -public class YaCyLegacyCredential extends Credential { - - private static final long serialVersionUID = -3527894085562480001L; - - private String hash; // remember password hash (for new style with prefix of used encryption supported "MD5:" ) - private String foruser; // remember the user as YaCy credential is username:pwd (not just pwd) - private boolean isBase64enc; // remember hash encoding false = encodeMD5Hex(usr:pwd) ; true = encodeMD5Hex(Base64Order.standardCoder.encodeString(usr:pw)) - private Credential c; - - /** - * internal hash function for admin account - * - * @param pw clear password - * @return hash string - */ - public static String calcHash(String pw) { // old style hash - return Digest.encodeMD5Hex(Base64Order.standardCoder.encodeString(pw)); - } - - @Override - public boolean check(Object credentials) { - - if (credentials instanceof Credential) { // for DIGEST auth - if (this.c == null) { - /* credential may be null after switching from BASIC to DIGEST authentication without re-encoding the password */ - return false; - } - Credential credential = (Credential) credentials; - return credential.check(this.c); - } - if (credentials instanceof String) { // for BASIC auth - final String pw = (String) credentials; - if (isBase64enc) { // for old B64MD5 admin hashes - if (serverAccessTracker.timeSinceAccessFromLocalhost() < 100) { - // we allow localhost accesses also to submit the hash as password - // this is very important since that method is used by the scripts in bin/ which are based on bin/apicall.sh - // the cleartext password is not stored anywhere, but we must find a way to allow scripts to steer a peer. - // this is the exception that makes that possible. - // TODO: it should be better to check the actual access IP here, but that is not handed over to Credential classes :( - if ((pw).equals(this.hash)) return true; - } - // exception for admin use old style MD5hash (user:password) - return calcHash(foruser + ":" + pw).equals(this.hash); // for admin user - } - - // normal users (and new admin pwd) for BASIC auth - if (hash.startsWith("MD5:") && hash != null) { - String realm = Switchboard.getSwitchboard().getConfig(SwitchboardConstants.ADMIN_REALM, ""); - boolean success = Digest.encodeMD5Hex(foruser + ":" + realm + ":" + pw).equals(hash.substring(4)); - // exception: allow the hash as pwd (used in bin/apicall.sh) - if (!success && foruser.equals(Switchboard.getSwitchboard().getConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME, "admin"))) { - if (pw.equals(hash)) { - if (serverAccessTracker.timeSinceAccessFromLocalhost() < 100) { - return true; - } - } - } - return success; - } - return Digest.encodeMD5Hex(foruser + ":" + pw).equals(hash); // for old userdb hashes - } - throw new UnsupportedOperationException(); - } - - /** - * create Credential object from config file hash - * - * @param configHash hash as in config file hash(adminuser:pwd) - * @return - */ - public static Credential getCredentialForAdmin(String username, String configHash) { - YaCyLegacyCredential yc = new YaCyLegacyCredential(); - if (configHash.startsWith("MD5:")) { - yc.isBase64enc = false; - yc.c = Credential.getCredential(configHash); - } else { - yc.isBase64enc = true; - } - yc.foruser = username; - yc.hash = configHash; - return yc; - } - -} diff --git a/source/net/yacy/http/YaCyLoginService.java b/source/net/yacy/http/YaCyLoginService.java index abc5ec2c7..ef9086cf3 100644 --- a/source/net/yacy/http/YaCyLoginService.java +++ b/source/net/yacy/http/YaCyLoginService.java @@ -88,7 +88,7 @@ public class YaCyLoginService extends HashLoginService implements LoginService { // in YaCy the credential hash is composed of username:pwd so the username is needed to create valid credential // not just the password (as usually in Jetty). As the accountname for the std. adminuser is not stored a useridentity // is created for current user (and the pwd checked against the stored username:pwd setting) - credential = YaCyLegacyCredential.getCredentialForAdmin(username, adminAccountBase64MD5); + credential = YaCyDigestCredential.getCredentialForAdmin(username, adminAccountBase64MD5); // TODO: YaCy user:pwd hashes should longterm likely be switched to separable username + pwd-hash entries // and/or the standard admin account username should be fix = "admin" roles = new String[]{SwitchboardConstants.ADMIN_ACCOUNT_ROLE}; diff --git a/source/net/yacy/http/YaCySecurityHandler.java b/source/net/yacy/http/YaCySecurityHandler.java index 675b02b35..436b167f1 100644 --- a/source/net/yacy/http/YaCySecurityHandler.java +++ b/source/net/yacy/http/YaCySecurityHandler.java @@ -24,11 +24,14 @@ package net.yacy.http; +import java.io.IOException; import java.net.MalformedURLException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + import net.yacy.cora.document.id.MultiProtocolURL; -import net.yacy.cora.order.Base64Order; -import net.yacy.cora.protocol.Domains; import net.yacy.cora.protocol.RequestHeader; import net.yacy.search.Switchboard; import net.yacy.search.SwitchboardConstants; @@ -42,70 +45,98 @@ import org.eclipse.jetty.server.Request; * jetty security handler * demands authentication for pages with _p. inside * and updates AccessTracker + * + * This is a thin adapter to the servlet container: the decision logic is in + * the container neutral {@link AdminSecurity}. */ public class YaCySecurityHandler extends ConstraintSecurityHandler { + /** + * Request-scoped hand-off of the client IP to the admin password check. + * <p> + * <b>What happens here and why:</b> YaCy accepts the stored admin password hash itself + * as a password, but only for requests originating from localhost. This is used by the + * {@code bin/*.sh} scripts (via {@code bin/apicall.sh}), which read the hash from the + * configuration file to steer a local peer - the cleartext password is never stored + * anywhere. That exception is evaluated in {@link YaCyDigestCredential#check(Object)}, + * which the servlet container invokes through the {@link YaCyLoginService}. + * <p> + * The problem: Jetty's {@link org.eclipse.jetty.util.security.Credential} API only + * receives the submitted password, not the request, so the credential can not tell on + * its own whether the request comes from localhost. This handler is the single place + * that both (a) sees the request and (b) spans the whole authentication of that request: + * {@code prepareConstraintInfo()}, the authenticator, the login service and finally + * {@code Credential.check()} all run synchronously inside {@code super.handle()} on this + * same thread. We therefore publish the request's true socket peer IP here for the + * duration of the request and clear it in a {@code finally} block (so it can not leak + * across pooled request threads). The credential check then reads a value that is bound + * to the exact request being authenticated. + * <p> + * This replaced a former process-global "last localhost access" timestamp with a 100ms + * window: that value was not tied to the request being checked and could be opened by + * unrelated concurrent localhost traffic. The IP used here is the real socket peer + * ({@link Request#getRemoteAddr()}); the client-controlled and spoofable X-Real-IP + * header is never consulted for this authentication decision. + */ + @Override + public void handle(final String pathInContext, final Request baseRequest, + final HttpServletRequest request, final HttpServletResponse response) + throws IOException, ServletException { + YaCyDigestCredential.setRequestClientIP(baseRequest.getRemoteAddr()); + try { + super.handle(pathInContext, baseRequest, request, response); + } finally { + YaCyDigestCredential.clearRequestClientIP(); + } + } + /** * create the constraint for the given path * for urls containing *_p. (like info_p.html) admin access is required, - * on localhost = admin setting no constraint is set + * on localhost = admin setting no constraint is set * @param pathInContext * @param request - * @return RoleInfo with + * @return RoleInfo with * isChecked=true if any security contraint applies (compare reference implementation org.eclipse.jetty.security.ConstraintSecurityHandler) * role = "admin" for resource name containint _p. */ @Override protected RoleInfo prepareConstraintInfo(String pathInContext, Request request) { final Switchboard sb = Switchboard.getSwitchboard(); - final boolean adminAccountGrantedForLocalhost = sb.getConfigBool(SwitchboardConstants.ADMIN_ACCOUNT_FOR_LOCALHOST, false); - final boolean adminAccountNeededForAllPages = sb.getConfigBool(SwitchboardConstants.ADMIN_ACCOUNT_All_PAGES, false); - String refererHost; - // update AccessTracker - final String remoteip = RequestHeader.client(request); + // Use the true socket peer IP for the access-control decision below, never the + // client-controlled (spoofable) X-Real-IP header that RequestHeader.client() would apply. + final String remoteip = request.getRemoteAddr(); serverAccessTracker.track(remoteip, pathInContext); + final boolean protectedPage = AdminSecurity.isProtectedPath(pathInContext, + sb.getConfigBool(SwitchboardConstants.ADMIN_ACCOUNT_All_PAGES, false), + sb.isRobinsonMode() && !sb.isPublicRobinson(), + sb.getConfigBool(SwitchboardConstants.PUBLIC_SEARCHPAGE, true)); + if (!protectedPage) { + return super.prepareConstraintInfo(pathInContext, request); + } + + String refererHost; try { refererHost = new MultiProtocolURL(request.getHeader(RequestHeader.REFERER)).getHost(); } catch (MalformedURLException e) { refererHost = null; } - final boolean accessFromLocalhost = Domains.isLocalhost(remoteip) && (refererHost == null || refererHost.length() == 0 || Domains.isLocalhost(refererHost)); - // ! note : accessFromLocalhost compares localhost ip pattern - final boolean grantedForLocalhost = adminAccountGrantedForLocalhost && accessFromLocalhost; - - // Even when all pages are protected, we don't want to block those used for peer-to-peer or cluster communication (except in private robinson mode) - // (examples : /yacy/hello.html is required for p2p and cluster network presence and /solr/select for remote Solr search requests) - boolean protectedPage = (adminAccountNeededForAllPages && ((sb.isRobinsonMode() && !sb.isPublicRobinson()) || - !(pathInContext.startsWith("/yacy/") || pathInContext.startsWith("/solr/")))); - - // Pages suffixed with "_p" are by the way always considered protected - protectedPage = protectedPage || (pathInContext.indexOf("_p.") > 0); - - // check "/gsa" and "/solr" if not publicSearchpage - if (!protectedPage && !sb.getConfigBool(SwitchboardConstants.PUBLIC_SEARCHPAGE, true)) { - protectedPage = pathInContext.startsWith("/solr/") || pathInContext.startsWith("/gsa/"); - } - - if (protectedPage) { - if (grantedForLocalhost) { + if (AdminSecurity.isLocalhostAccess(remoteip, refererHost)) { + if (sb.getConfigBool(SwitchboardConstants.ADMIN_ACCOUNT_FOR_LOCALHOST, false)) { + return null; + } + // last chance to authorize using the admin from localhost + if (AdminSecurity.checkLocalhostLazyAuth(request.getHeader(RequestHeader.AUTHORIZATION), + sb.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME, "admin"), + sb.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, ""))) { return null; - } else if (accessFromLocalhost) { - // last chance to authorize using the admin from localhost - final String adminAccountBase64MD5 = sb.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, ""); - final String credentials = request.getHeader(RequestHeader.AUTHORIZATION); - if (credentials != null && credentials.length() < 120 && credentials.startsWith("Basic ")) { // Basic credentials are short "Basic " + b64(user:pwd) - final String foruser = sb.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME, "admin"); - final String b64 = Base64Order.standardCoder.encodeString(foruser + ":" + adminAccountBase64MD5); // TODO: is this valid? ; consider "MD5:" prefixed config - if ((credentials.substring(6)).equals(b64)) return null; // lazy authentication for local access with credential from config (only a user with read access to DATA can do that) - } } - RoleInfo roleinfo = new RoleInfo(); - roleinfo.setChecked(true); // RoleInfo.setChecked() : in Jetty this means - marked to have any security constraint - roleinfo.addRole(SwitchboardConstants.ADMIN_ACCOUNT_ROLE); - return roleinfo; } - return super.prepareConstraintInfo(pathInContext, request); + RoleInfo roleinfo = new RoleInfo(); + roleinfo.setChecked(true); // RoleInfo.setChecked() : in Jetty this means - marked to have any security constraint + roleinfo.addRole(SwitchboardConstants.ADMIN_ACCOUNT_ROLE); + return roleinfo; } } diff --git a/source/net/yacy/search/Switchboard.java b/source/net/yacy/search/Switchboard.java index 13fee0653..c0d189b2e 100644 --- a/source/net/yacy/search/Switchboard.java +++ b/source/net/yacy/search/Switchboard.java @@ -3929,7 +3929,10 @@ public final class Switchboard extends serverSwitch { // handle DIGEST auth by servlet container if (requestHeader.getUserPrincipal() != null) { // user is authenticated (by Servlet container) if (requestHeader.isUserInRole(SwitchboardConstants.ADMIN_ACCOUNT_ROLE)) { - // we could double check admin right (but we trust embedded container) + // DIGEST can't be re-verified from the header (response depends on the + // server nonce); only the embedded container that issued the challenge can. + // Trusting its result is safe because getUserPrincipal()/isUserInRole() come + // solely from container auth (see RequestHeader). BASIC above verifies itself. this.adminAuthenticationLastAccess = System.currentTimeMillis(); return 4; // has admin right } diff --git a/source/net/yacy/server/serverAccessTracker.java b/source/net/yacy/server/serverAccessTracker.java index d4f13817d..dce65b7f3 100644 --- a/source/net/yacy/server/serverAccessTracker.java +++ b/source/net/yacy/server/serverAccessTracker.java @@ -28,7 +28,6 @@ import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingQueue; -import net.yacy.cora.protocol.Domains; public class serverAccessTracker { @@ -38,7 +37,6 @@ public class serverAccessTracker { private static int maxHostCount = 100; private static final ConcurrentHashMap<String, Queue<Track>> accessTracker = new ConcurrentHashMap<String, Queue<Track>>(); // mappings from requesting host to an ArrayList of serverTrack-entries private static long lastCleanup; - private static long lastLocalhostAccess = 0; public static class Track { private final long time; @@ -140,7 +138,6 @@ public class serverAccessTracker { track.add(new Track(System.currentTimeMillis(), accessPath)); clearTooOldAccess(track); } - if (Domains.isLocalhost(host)) lastLocalhostAccess = System.currentTimeMillis(); } public static Collection<Track> accessTrack(final String host) { @@ -162,8 +159,4 @@ public class serverAccessTracker { accessTrackerClone.putAll(accessTracker); return accessTrackerClone.keySet().iterator(); } - - public static long timeSinceAccessFromLocalhost() { - return System.currentTimeMillis() - lastLocalhostAccess; - } } |
