Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/main/java/org/jenkinsci/plugins/badge/StatusImage.java
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,9 @@ class StatusImage implements HttpResponse {
URL url = new URL(link);
final String protocol = url.getProtocol();
if ("http".equals(protocol) || "https".equals(protocol)) {
linkCode = "<svg onclick=\"window.open(&quot;"
linkCode = "<svg class=\"jenkins-badge-clickable\" data-jenkins-link-url=\""
+ link
+ "&quot;);\" style=\"cursor: pointer;\" xmlns";
+ "\" style=\"cursor: pointer;\" xmlns";
} else {
LOGGER.log(Level.FINE, "Invalid link protocol: {0}", protocol);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<l:layout title="${%Embeddable Build Status Icon}" type="one-column">
<l:header>
<link rel='stylesheet' href='${app.rootUrl}/plugin/embeddable-build-status/css/design.css' type='text/css'/>
<script src='${app.rootUrl}/plugin/embeddable-build-status/js/badge-click-handler.js' type='text/javascript'></script>
</l:header>
<l:main-panel>
<h1>${%Embeddable Build Status Icon}</h1>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<l:layout title="${%Embeddable Build Status Icon}" type="one-column">
<l:header>
<link rel='stylesheet' href='${app.rootUrl}/plugin/embeddable-build-status/css/design.css' type='text/css'/>
<script src='${app.rootUrl}/plugin/embeddable-build-status/js/badge-click-handler.js' type='text/javascript'></script>
</l:header>
<l:main-panel>
<h1>${%Embeddable Build Status Icon}</h1>
Expand Down
10 changes: 10 additions & 0 deletions src/main/webapp/js/badge-click-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
document.addEventListener('click', function(event) {
var svg = event.target.closest('svg.jenkins-badge-clickable');
if (svg) {
var url = svg.getAttribute('data-jenkins-link-url');
if (url) {
window.open(url);
event.preventDefault();
}
}
});
205 changes: 205 additions & 0 deletions src/test/java/org/jenkinsci/plugins/badge/StatusImageTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThan;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;

import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
Expand Down Expand Up @@ -365,4 +367,207 @@ void testFontLoadingRobustness() throws Exception {
assertThat("Insufficient width for input: " + input, width, greaterThan(input.length() * 5));
}
}

// ========================================================================
// CSP Compliance Tests - Added for inline event handler removal
// ========================================================================

@Test
void testStatusImageWithValidLinkHasCSPCompliantAttributes() throws Exception {
String subject = "build";
String status = "passing";
String colorName = "brightgreen";
String animatedColorName = null;
String style = "flat";
String link = "https://example.com/build";

StatusImage statusImage = new StatusImage(subject, status, colorName, animatedColorName, style, link);
String svgContent = getPayloadAsString(statusImage);

// Verify CSP-compliant attributes are present
assertThat(
"SVG should have jenkins-badge-clickable class",
svgContent,
containsString("class=\"jenkins-badge-clickable\""));
assertThat(
"SVG should have data-jenkins-link-url attribute",
svgContent,
containsString("data-jenkins-link-url=\"" + link + "\""));
assertThat("SVG should have cursor pointer style", svgContent, containsString("style=\"cursor: pointer;\""));

// Verify no inline event handlers
assertThat("SVG should not contain onclick attribute", svgContent, not(containsString("onclick")));
assertThat("SVG should not contain window.open call", svgContent, not(containsString("window.open")));
}

@Test
void testStatusImageWithHttpLinkHasCSPCompliantAttributes() throws Exception {
String subject = "test";
String status = "failing";
String colorName = "red";
String animatedColorName = null;
String style = "plastic";
String link = "http://insecure.example.com/build";

StatusImage statusImage = new StatusImage(subject, status, colorName, animatedColorName, style, link);
String svgContent = getPayloadAsString(statusImage);

// Verify CSP-compliant attributes are present for HTTP links too
assertThat(
"SVG should have jenkins-badge-clickable class for HTTP links",
svgContent,
containsString("class=\"jenkins-badge-clickable\""));
assertThat(
"SVG should have data-jenkins-link-url attribute for HTTP links",
svgContent,
containsString("data-jenkins-link-url=\"" + link + "\""));

// Verify no inline event handlers
assertThat(
"SVG should not contain onclick attribute for HTTP links", svgContent, not(containsString("onclick")));
}

@Test
void testStatusImageWithNullLinkHasNoCSPAttributes() throws Exception {
String subject = "build";
String status = "passing";
String colorName = "brightgreen";
String animatedColorName = null;
String style = "flat";
String link = null;

StatusImage statusImage = new StatusImage(subject, status, colorName, animatedColorName, style, link);
String svgContent = getPayloadAsString(statusImage);

// Verify no CSP attributes when no link provided
assertThat(
"SVG should not have jenkins-badge-clickable class when no link",
svgContent,
not(containsString("jenkins-badge-clickable")));
assertThat(
"SVG should not have data-jenkins-link-url attribute when no link",
svgContent,
not(containsString("data-jenkins-link-url")));
assertThat(
"SVG should not have cursor pointer style when no link",
svgContent,
not(containsString("cursor: pointer")));

// Verify no inline event handlers
assertThat("SVG should not contain onclick attribute when no link", svgContent, not(containsString("onclick")));
}

@Test
void testStatusImageWithInvalidProtocolHasNoCSPAttributes() throws Exception {
String subject = "build";
String status = "passing";
String colorName = "brightgreen";
String animatedColorName = null;
String style = "flat";
String link = "javascript:alert('xss')";

StatusImage statusImage = new StatusImage(subject, status, colorName, animatedColorName, style, link);
String svgContent = getPayloadAsString(statusImage);

// Verify no CSP attributes when invalid protocol provided
assertThat(
"SVG should not have jenkins-badge-clickable class for invalid protocol",
svgContent,
not(containsString("jenkins-badge-clickable")));
assertThat(
"SVG should not have data-jenkins-link-url attribute for invalid protocol",
svgContent,
not(containsString("data-jenkins-link-url")));
assertThat(
"SVG should not have cursor pointer style for invalid protocol",
svgContent,
not(containsString("cursor: pointer")));

// Verify no inline event handlers
assertThat(
"SVG should not contain onclick attribute for invalid protocol",
svgContent,
not(containsString("onclick")));
}

@Test
void testStatusImageWithMalformedLinkHasNoCSPAttributes() throws Exception {
String subject = "build";
String status = "passing";
String colorName = "brightgreen";
String animatedColorName = null;
String style = "flat";
String link = "not-a-valid-url";

StatusImage statusImage = new StatusImage(subject, status, colorName, animatedColorName, style, link);
String svgContent = getPayloadAsString(statusImage);

// Verify no CSP attributes when malformed URL provided
assertThat(
"SVG should not have jenkins-badge-clickable class for malformed URL",
svgContent,
not(containsString("jenkins-badge-clickable")));
assertThat(
"SVG should not have data-jenkins-link-url attribute for malformed URL",
svgContent,
not(containsString("data-jenkins-link-url")));
assertThat(
"SVG should not have cursor pointer style for malformed URL",
svgContent,
not(containsString("cursor: pointer")));

// Verify no inline event handlers
assertThat(
"SVG should not contain onclick attribute for malformed URL",
svgContent,
not(containsString("onclick")));
}

@Test
void testStatusImageWithLinkEscapesHtmlProperly() throws Exception {
String subject = "build";
String status = "passing";
String colorName = "brightgreen";
String animatedColorName = null;
String style = "flat";
String link = "https://example.com/path?param=\"value\"&other=<test>";

StatusImage statusImage = new StatusImage(subject, status, colorName, animatedColorName, style, link);
String svgContent = getPayloadAsString(statusImage);

// Verify HTML escaping is preserved in the link attribute (double-escaped as per StatusImage.java line 106)
assertThat(
"SVG should have properly escaped link URL",
svgContent,
containsString(
"data-jenkins-link-url=\"https://example.com/path?param=&amp;quot;value&amp;quot;&amp;amp;other=&amp;lt;test&amp;gt;\""));

// Verify CSP-compliant attributes are still present
assertThat(
"SVG should still have jenkins-badge-clickable class with special characters",
svgContent,
containsString("class=\"jenkins-badge-clickable\""));

// Verify no inline event handlers
assertThat(
"SVG should not contain onclick attribute with special characters",
svgContent,
not(containsString("onclick")));
}

/**
* Helper method to access the payload for testing
*/
private byte[] getPayload(StatusImage statusImage) throws Exception {
Field payloadField = StatusImage.class.getDeclaredField("payload");
payloadField.setAccessible(true);
return (byte[]) payloadField.get(statusImage);
}

/**
* Test helper method to get payload content
*/
private String getPayloadAsString(StatusImage statusImage) throws Exception {
return new String(getPayload(statusImage), StandardCharsets.UTF_8);
}
}