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 /test | |
| parent | dd0d4ee962dfc50105c0465b8ea0dc2310e601d2 (diff) | |
de-coupling of security functions from jetty
Diffstat (limited to 'test')
| -rw-r--r-- | test/java/net/yacy/cora/protocol/RequestHeaderTest.java | 58 | ||||
| -rw-r--r-- | test/java/net/yacy/http/AdminSecurityTest.java | 109 |
2 files changed, 167 insertions, 0 deletions
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.*;
@@ -30,6 +36,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.
*/
@Test
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")); + } +} |
