|
45 | 45 | import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript; |
46 | 46 | import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.TestGroovyRecorder; |
47 | 47 | import org.jenkinsci.plugins.scriptsecurity.scripts.languages.GroovyLanguage; |
| 48 | +import edu.umd.cs.findbugs.annotations.NonNull; |
48 | 49 | import org.junit.Rule; |
49 | 50 | import org.junit.Test; |
50 | 51 | import org.jvnet.hudson.test.Issue; |
|
55 | 56 | import org.xml.sax.SAXException; |
56 | 57 |
|
57 | 58 | import java.io.IOException; |
| 59 | +import java.io.OutputStream; |
| 60 | +import java.net.InetSocketAddress; |
58 | 61 | import java.net.URL; |
| 62 | +import java.nio.charset.StandardCharsets; |
59 | 63 | import java.util.List; |
60 | 64 | import java.util.concurrent.atomic.AtomicLong; |
61 | 65 | import java.util.logging.Level; |
| 66 | +import com.sun.net.httpserver.HttpServer; |
62 | 67 |
|
63 | 68 | import static org.hamcrest.MatcherAssert.assertThat; |
64 | 69 | import static org.hamcrest.Matchers.hasItemInArray; |
@@ -128,19 +133,26 @@ public void malformedScriptApproval() throws Exception { |
128 | 133 |
|
129 | 134 | @Issue("SECURITY-1866") |
130 | 135 | @Test public void classpathEntriesEscaped() throws Exception { |
131 | | - // Add pending classpath entry. |
132 | | - final UnapprovedClasspathException e = assertThrows(UnapprovedClasspathException.class, () -> |
133 | | - ScriptApproval.get().using(new ClasspathEntry("https://www.example.com/#value=Hack<img id='xss' src=x onerror=alert(123)>Hack"))); |
134 | | - |
135 | | - // Check for XSS in pending approvals. |
136 | | - JenkinsRule.WebClient wc = r.createWebClient(); |
137 | | - HtmlPage approvalPage = wc.goTo("scriptApproval"); |
138 | | - assertThat(approvalPage.getElementById("xss"), nullValue()); |
139 | | - // Approve classpath entry. |
140 | | - ScriptApproval.get().approveClasspathEntry(e.getHash()); |
141 | | - // Check for XSS in approved classpath entries. |
142 | | - HtmlPage approvedPage = wc.goTo("scriptApproval"); |
143 | | - assertThat(approvedPage.getElementById("xss"), nullValue()); |
| 136 | + HttpServer mockJarServer = createAndStartMockJarHttpServer(); |
| 137 | + String mockJarUrl = "http:/" + mockJarServer.getAddress() + "/library.jar#value=Hack<img id='xss' src=x onerror=alert(123)>Hack"; |
| 138 | + |
| 139 | + try { |
| 140 | + // Add pending classpath entry. |
| 141 | + final UnapprovedClasspathException e = assertThrows(UnapprovedClasspathException.class, () -> |
| 142 | + ScriptApproval.get().using(new ClasspathEntry(mockJarUrl))); |
| 143 | + |
| 144 | + // Check for XSS in pending approvals. |
| 145 | + JenkinsRule.WebClient wc = r.createWebClient(); |
| 146 | + HtmlPage approvalPage = wc.goTo("scriptApproval"); |
| 147 | + assertThat(approvalPage.getElementById("xss"), nullValue()); |
| 148 | + // Approve classpath entry. |
| 149 | + ScriptApproval.get().approveClasspathEntry(e.getHash()); |
| 150 | + // Check for XSS in approved classpath entries. |
| 151 | + HtmlPage approvedPage = wc.goTo("scriptApproval"); |
| 152 | + assertThat(approvedPage.getElementById("xss"), nullValue()); |
| 153 | + }finally { |
| 154 | + mockJarServer.stop(0); |
| 155 | + } |
144 | 156 | } |
145 | 157 |
|
146 | 158 | @Test public void clearMethodsLifeCycle() throws Exception { |
@@ -209,71 +221,102 @@ public void reload() throws Exception { |
209 | 221 | r.assertBuildStatus(Result.FAILURE, p.scheduleBuild2(0).get())); |
210 | 222 | } |
211 | 223 |
|
| 224 | + |
212 | 225 | @Test |
213 | 226 | public void forceSandboxTests() throws Exception { |
214 | 227 | setBasicSecurity(); |
215 | 228 |
|
216 | | - try (ACLContext ctx = ACL.as(User.getById("devel", true))) { |
217 | | - assertTrue(ScriptApproval.get().isForceSandbox()); |
218 | | - assertTrue(ScriptApproval.get().isForceSandboxForCurrentUser()); |
| 229 | + HttpServer mockJarServer = createAndStartMockJarHttpServer(); |
| 230 | + String mockJarUrl = "http:/" + mockJarServer.getAddress() + "/library.jar"; |
219 | 231 |
|
220 | | - final ApprovalContext ac = ApprovalContext.create(); |
| 232 | + try { |
| 233 | + // Test with non-admin user |
| 234 | + try (ACLContext ctx = ACL.as(User.getById("devel", true))) { |
| 235 | + assertTrue(ScriptApproval.get().isForceSandbox()); |
| 236 | + assertTrue(ScriptApproval.get().isForceSandboxForCurrentUser()); |
221 | 237 |
|
222 | | - //Insert new PendingScript - As the user is not admin and ForceSandbox is enabled, nothing should be added |
223 | | - { |
224 | | - ScriptApproval.get().configuring("testScript", GroovyLanguage.get(), ac, true); |
225 | | - assertTrue(ScriptApproval.get().getPendingScripts().isEmpty()); |
226 | | - } |
| 238 | + final ApprovalContext ac = ApprovalContext.create(); |
227 | 239 |
|
228 | | - //Insert new PendingSignature - As the user is not admin and ForceSandbox is enabled, nothing should be added |
229 | | - { |
230 | | - ScriptApproval.get().accessRejected( |
231 | | - new RejectedAccessException("testSignatureType", "testSignatureDetails"), ac); |
232 | | - assertTrue(ScriptApproval.get().getPendingSignatures().isEmpty()); |
233 | | - } |
| 240 | + //Insert new PendingScript - As the user is not admin and ForceSandbox is enabled, nothing should be added |
| 241 | + { |
| 242 | + ScriptApproval.get().configuring("testScript", GroovyLanguage.get(), ac, true); |
| 243 | + assertTrue(ScriptApproval.get().getPendingScripts().isEmpty()); |
| 244 | + } |
234 | 245 |
|
235 | | - //Insert new Pending Classpath - As the user is not admin and ForceSandbox is enabled, nothing should be added |
236 | | - { |
237 | | - ClasspathEntry cpe = new ClasspathEntry("https://www.jenkins.io"); |
238 | | - ScriptApproval.get().configuring(cpe, ac); |
239 | | - ScriptApproval.get().addPendingClasspathEntry( |
240 | | - new ScriptApproval.PendingClasspathEntry("hash", new URL("https://www.jenkins.io"), ac)); |
241 | | - assertThrows(UnapprovedClasspathException.class, () -> ScriptApproval.get().using(cpe)); |
242 | | - // As we are forcing sandbox, none of the previous operations are able to create new pending ClasspathEntries |
243 | | - assertTrue(ScriptApproval.get().getPendingClasspathEntries().isEmpty()); |
| 246 | + //Insert new PendingSignature - As the user is not admin and ForceSandbox is enabled, nothing should be added |
| 247 | + { |
| 248 | + ScriptApproval.get().accessRejected( |
| 249 | + new RejectedAccessException("testSignatureType", "testSignatureDetails"), ac); |
| 250 | + assertTrue(ScriptApproval.get().getPendingSignatures().isEmpty()); |
| 251 | + } |
| 252 | + |
| 253 | + //Insert new Pending Classpath - As the user is not admin and ForceSandbox is enabled, nothing should be added |
| 254 | + { |
| 255 | + ClasspathEntry cpe = new ClasspathEntry(mockJarUrl); |
| 256 | + ScriptApproval.get().configuring(cpe, ac); |
| 257 | + ScriptApproval.get().addPendingClasspathEntry( |
| 258 | + new ScriptApproval.PendingClasspathEntry("hash", new URL(mockJarUrl), ac)); |
| 259 | + assertThrows(UnapprovedClasspathException.class, () -> ScriptApproval.get().using(cpe)); |
| 260 | + // As we are forcing sandbox, none of the previous operations are able to create new pending ClasspathEntries |
| 261 | + assertTrue(ScriptApproval.get().getPendingClasspathEntries().isEmpty()); |
| 262 | + } |
244 | 263 | } |
245 | | - } |
246 | 264 |
|
247 | | - try (ACLContext ctx = ACL.as(User.getById("admin", true))) { |
248 | | - assertTrue(ScriptApproval.get().isForceSandbox()); |
249 | | - assertFalse(ScriptApproval.get().isForceSandboxForCurrentUser()); |
| 265 | + // Test with admin user |
| 266 | + try (ACLContext ctx = ACL.as(User.getById("admin", true))) { |
| 267 | + assertTrue(ScriptApproval.get().isForceSandbox()); |
| 268 | + assertFalse(ScriptApproval.get().isForceSandboxForCurrentUser()); |
250 | 269 |
|
251 | | - final ApprovalContext ac = ApprovalContext.create(); |
| 270 | + final ApprovalContext ac = ApprovalContext.create(); |
252 | 271 |
|
253 | | - //Insert new PendingScript - As the user is admin, the behavior does not change |
254 | | - { |
255 | | - ScriptApproval.get().configuring("testScript", GroovyLanguage.get(), ac, true); |
256 | | - assertEquals(1, ScriptApproval.get().getPendingScripts().size()); |
257 | | - } |
| 272 | + //Insert new PendingScript - As the user is admin, the behavior does not change |
| 273 | + { |
| 274 | + ScriptApproval.get().configuring("testScript", GroovyLanguage.get(), ac, true); |
| 275 | + assertEquals(1, ScriptApproval.get().getPendingScripts().size()); |
| 276 | + } |
258 | 277 |
|
259 | | - //Insert new PendingSignature - - As the user is admin, the behavior does not change |
260 | | - { |
261 | | - ScriptApproval.get().accessRejected( |
262 | | - new RejectedAccessException("testSignatureType", "testSignatureDetails"), ac); |
263 | | - assertEquals(1, ScriptApproval.get().getPendingSignatures().size()); |
264 | | - } |
| 278 | + //Insert new PendingSignature - - As the user is admin, the behavior does not change |
| 279 | + { |
| 280 | + ScriptApproval.get().accessRejected( |
| 281 | + new RejectedAccessException("testSignatureType", "testSignatureDetails"), ac); |
| 282 | + assertEquals(1, ScriptApproval.get().getPendingSignatures().size()); |
| 283 | + } |
265 | 284 |
|
266 | | - //Insert new Pending ClassPatch - - As the user is admin, the behavior does not change |
267 | | - { |
268 | | - ClasspathEntry cpe = new ClasspathEntry("https://www.jenkins.io"); |
269 | | - ScriptApproval.get().configuring(cpe, ac); |
270 | | - ScriptApproval.get().addPendingClasspathEntry( |
271 | | - new ScriptApproval.PendingClasspathEntry("hash", new URL("https://www.jenkins.io"), ac)); |
272 | | - assertEquals(1, ScriptApproval.get().getPendingClasspathEntries().size()); |
| 285 | + //Insert new Pending ClassPatch - - As the user is admin, the behavior does not change |
| 286 | + { |
| 287 | + ClasspathEntry cpe = new ClasspathEntry(mockJarUrl); |
| 288 | + ScriptApproval.get().configuring(cpe, ac); |
| 289 | + ScriptApproval.get().addPendingClasspathEntry( |
| 290 | + new ScriptApproval.PendingClasspathEntry("hash", new URL(mockJarUrl), ac)); |
| 291 | + assertEquals(1, ScriptApproval.get().getPendingClasspathEntries().size()); |
| 292 | + } |
273 | 293 | } |
| 294 | + } finally { |
| 295 | + mockJarServer.stop(0); |
274 | 296 | } |
275 | 297 | } |
276 | 298 |
|
| 299 | + /** |
| 300 | + * Creates and starts a mock HTTP server that serves a dummy JAR file at the path "/library.jar". |
| 301 | + * <p> |
| 302 | + * The server listens on a random available port on localhost and responds with a simple string |
| 303 | + * as the JAR content. This is useful for tests that require a remote JAR resource. |
| 304 | + * </p> |
| 305 | + */ |
| 306 | + private static @NonNull HttpServer createAndStartMockJarHttpServer() throws IOException { |
| 307 | + HttpServer mockServer = HttpServer.create(new InetSocketAddress("localhost", 0), 0); |
| 308 | + mockServer.createContext("/library.jar", exchange -> { |
| 309 | + byte[] responseBytes = "Mock JAR content".getBytes(StandardCharsets.UTF_8); |
| 310 | + exchange.sendResponseHeaders(200, responseBytes.length); |
| 311 | + try (exchange; OutputStream os = exchange.getResponseBody()) { |
| 312 | + os.write(responseBytes); |
| 313 | + } |
| 314 | + }); |
| 315 | + mockServer.setExecutor(null); |
| 316 | + mockServer.start(); |
| 317 | + return mockServer; |
| 318 | + } |
| 319 | + |
277 | 320 | @Test |
278 | 321 | public void forceSandboxScriptSignatureException() throws Exception { |
279 | 322 | ScriptApproval.get().setForceSandbox(true); |
|
0 commit comments