From edeb6162c435e4ad9098233eed4f8c9c9b0312e2 Mon Sep 17 00:00:00 2001 From: Michael Peter Christen Date: Sat, 11 Jul 2026 20:52:05 +0200 Subject: de-coupling of security functions from jetty --- .../net/yacy/cora/protocol/RequestHeaderTest.java | 58 +++++++++++ test/java/net/yacy/http/AdminSecurityTest.java | 109 +++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 test/java/net/yacy/http/AdminSecurityTest.java (limited to 'test/java') diff --git a/test/java/net/yacy/cora/protocol/RequestHeaderTest.java b/test/java/net/yacy/cora/protocol/RequestHeaderTest.java index 3cdb23cb9..0931d8e0c 100644 --- a/test/java/net/yacy/cora/protocol/RequestHeaderTest.java +++ b/test/java/net/yacy/cora/protocol/RequestHeaderTest.java @@ -19,6 +19,12 @@ */ package net.yacy.cora.protocol; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import javax.servlet.http.HttpServletRequest; + import org.junit.Test; import static org.junit.Assert.*; @@ -29,6 +35,58 @@ import static org.junit.Assert.*; */ public class RequestHeaderTest { + /** + * Build a minimal HttpServletRequest stub answering getRemoteAddr() with the + * given socket peer address and returning the given X-Real-IP header value. + */ + private static HttpServletRequest stubRequest(final String socketPeer, final String xRealIP) { + final InvocationHandler h = new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) { + switch (method.getName()) { + case "getRemoteAddr": + return socketPeer; + case "getRemoteHost": + return socketPeer; + case "getHeader": + return RequestHeader.X_Real_IP.equals(args[0]) ? xRealIP : null; + default: + return null; + } + } + }; + return (HttpServletRequest) Proxy.newProxyInstance( + RequestHeaderTest.class.getClassLoader(), + new Class[]{HttpServletRequest.class}, h); + } + + /** + * Authentication must rely on the true socket peer, never on the spoofable + * X-Real-IP header. A remote client sending "X-Real-IP: 127.0.0.1" must not + * be treated as localhost. + */ + @Test + public void testXRealIpDoesNotAffectAuthentication() { + final String remoteClient = "203.0.113.7"; // a non-local address (TEST-NET-3) + + // spoofing attempt: remote socket peer, but X-Real-IP claims localhost + final RequestHeader spoofed = new RequestHeader(stubRequest(remoteClient, "127.0.0.1")); + // routing accessor honors the header (kept for peer routing behind a trusted proxy) ... + assertEquals("127.0.0.1", spoofed.getRemoteAddr()); + // ... but the authentication accessor must return the true socket peer + assertEquals(remoteClient, spoofed.getRemoteSocketAddr()); + assertFalse("spoofed X-Real-IP must not grant localhost access", spoofed.accessFromLocalhost()); + + // genuine localhost access still works + final RequestHeader local = new RequestHeader(stubRequest("127.0.0.1", null)); + assertEquals("127.0.0.1", local.getRemoteSocketAddr()); + assertTrue(local.accessFromLocalhost()); + + // remote client without spoofing stays remote + final RequestHeader remote = new RequestHeader(stubRequest(remoteClient, null)); + assertFalse(remote.accessFromLocalhost()); + } + /** * Test of getServerPort method, of class RequestHeader. */ diff --git a/test/java/net/yacy/http/AdminSecurityTest.java b/test/java/net/yacy/http/AdminSecurityTest.java new file mode 100644 index 000000000..4fd8abb58 --- /dev/null +++ b/test/java/net/yacy/http/AdminSecurityTest.java @@ -0,0 +1,109 @@ +package net.yacy.http; + +import org.junit.Assert; +import org.junit.Test; + +import net.yacy.cora.order.Base64Order; +import net.yacy.cora.order.Digest; + +/** + * Unit tests for the servlet container neutral admin security decision logic, + * especially the supported admin password hash formats. + */ +public class AdminSecurityTest { + + /** + * Test which paths demand admin rights depending on configuration. + */ + @Test + public void testIsProtectedPath() { + // pages suffixed with "_p" are always protected + Assert.assertTrue(AdminSecurity.isProtectedPath("/Settings_p.html", false, false, true)); + Assert.assertTrue(AdminSecurity.isProtectedPath("/api/table_p.xml", false, false, true)); + // normal pages are public by default + Assert.assertFalse(AdminSecurity.isProtectedPath("/index.html", false, false, true)); + Assert.assertFalse(AdminSecurity.isProtectedPath("/yacysearch.html", false, false, true)); + + // adminForAllPages protects everything ... + Assert.assertTrue(AdminSecurity.isProtectedPath("/index.html", true, false, true)); + // ... except the p2p and remote search interfaces ... + Assert.assertFalse(AdminSecurity.isProtectedPath("/yacy/hello.html", true, false, true)); + Assert.assertFalse(AdminSecurity.isProtectedPath("/solr/select", true, false, true)); + // ... which are protected too in private robinson mode + Assert.assertTrue(AdminSecurity.isProtectedPath("/yacy/hello.html", true, true, true)); + Assert.assertTrue(AdminSecurity.isProtectedPath("/solr/select", true, true, true)); + + // without public search page the solr and gsa interfaces are protected + Assert.assertTrue(AdminSecurity.isProtectedPath("/solr/select", false, false, false)); + Assert.assertTrue(AdminSecurity.isProtectedPath("/gsa/search", false, false, false)); + Assert.assertFalse(AdminSecurity.isProtectedPath("/index.html", false, false, false)); + } + + /** + * Test the Base64 based admin password hash format MD5Hex(Base64(user:password)). + */ + @Test + public void testCheckAdminPasswordBase64Format() { + final String user = "admin"; + final String pw = "secret"; + final String configHash = AdminSecurity.calcHash(user + ":" + pw); + + Assert.assertTrue(AdminSecurity.checkAdminPassword(user, configHash, "YaCy", "admin", false, pw)); + Assert.assertFalse(AdminSecurity.checkAdminPassword(user, configHash, "YaCy", "admin", false, "wrong")); + Assert.assertFalse(AdminSecurity.checkAdminPassword(user, configHash, "YaCy", "admin", false, "")); + + // the hash itself is accepted as password, but only on recent localhost access (bin/apicall.sh) + Assert.assertTrue(AdminSecurity.checkAdminPassword(user, configHash, "YaCy", "admin", true, configHash)); + Assert.assertFalse(AdminSecurity.checkAdminPassword(user, configHash, "YaCy", "admin", false, configHash)); + } + + /** + * Test the "MD5:" prefixed admin password hash format MD5Hex(user:realm:password). + */ + @Test + public void testCheckAdminPasswordDigestFormat() { + final String user = "admin"; + final String pw = "secret"; + final String realm = "YaCy"; + final String configHash = "MD5:" + Digest.encodeMD5Hex(user + ":" + realm + ":" + pw); + + Assert.assertTrue(AdminSecurity.checkAdminPassword(user, configHash, realm, "admin", false, pw)); + Assert.assertFalse(AdminSecurity.checkAdminPassword(user, configHash, realm, "admin", false, "wrong")); + // the realm is part of the hash: a different realm must not verify + Assert.assertFalse(AdminSecurity.checkAdminPassword(user, configHash, "OtherRealm", "admin", false, pw)); + + // the full config hash is accepted as password for the admin user on recent localhost access (bin/apicall.sh) + Assert.assertTrue(AdminSecurity.checkAdminPassword(user, configHash, realm, "admin", true, configHash)); + Assert.assertFalse(AdminSecurity.checkAdminPassword(user, configHash, realm, "admin", false, configHash)); + // but not for another user name + Assert.assertFalse(AdminSecurity.checkAdminPassword("other", configHash, realm, "admin", true, configHash)); + } + + /** + * Test the lazy localhost authorization with the config hash as Basic credential. + */ + @Test + public void testCheckLocalhostLazyAuth() { + final String adminUser = "admin"; + final String configHash = "MD5:0cef3f723bbf6ec22bbb0ca4d4dfd001"; + final String validHeader = "Basic " + Base64Order.standardCoder.encodeString(adminUser + ":" + configHash); + + Assert.assertTrue(AdminSecurity.checkLocalhostLazyAuth(validHeader, adminUser, configHash)); + Assert.assertFalse(AdminSecurity.checkLocalhostLazyAuth(null, adminUser, configHash)); + Assert.assertFalse(AdminSecurity.checkLocalhostLazyAuth("Basic d3Jvbmc=", adminUser, configHash)); + Assert.assertFalse(AdminSecurity.checkLocalhostLazyAuth("Digest something", adminUser, configHash)); + } + + /** + * Test the localhost access check including the referer host condition. + */ + @Test + public void testIsLocalhostAccess() { + Assert.assertTrue(AdminSecurity.isLocalhostAccess("127.0.0.1", null)); + Assert.assertTrue(AdminSecurity.isLocalhostAccess("127.0.0.1", "")); + Assert.assertTrue(AdminSecurity.isLocalhostAccess("127.0.0.1", "localhost")); + Assert.assertFalse(AdminSecurity.isLocalhostAccess("192.0.2.17", null)); + // a request from localhost referred by a remote page is not a localhost access + Assert.assertFalse(AdminSecurity.isLocalhostAccess("127.0.0.1", "example.org")); + } +} -- cgit v1.2.3