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
42 changes: 42 additions & 0 deletions java/jenkins/csrf/statechange-without-requirepost.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
rules:
- id: codevigilant.java.jenkins.csrf.statechange-without-requirepost
patterns:
- pattern-inside: |
public $RET $HANDLER(...) {
...
}
- metavariable-regex:
metavariable: $HANDLER
regex: ^do[A-Z]
- pattern-either:
- pattern: $OBJ.scheduleBuild2($DELAY, $CAUSE, $ACTIONS);
- pattern: $OBJ.scheduleBuild($DELAY);
- pattern: $OBJ.schedule2($DELAY);
- pattern: $EXEC.interrupt($RESULT);
- pattern: $QUEUE.setSorter($SORTER);
- pattern-not-inside: |
@RequirePOST
public $RET $HANDLER(...) {
...
}
message: |
Detected a Stapler 'do*' handler that mutates Jenkins state (build
scheduling, executor interruption, queue sorter replacement) without the
@RequirePOST annotation. Stapler routes unannotated 'do*' methods for
both GET and POST, and the CSRF crumb is only enforced on POST requests,
so a cross-site GET (e.g. an <img> tag pointing at the handler URL) can
trigger the state change with the victim's credentials. Annotate the
handler with @RequirePOST so it rejects GET and requires a valid crumb.
metadata:
category: security
cwe: "CWE-352: Cross-Site Request Forgery"
owasp: "A01:2021 - Broken Access Control"
technology: jenkins
confidence: MEDIUM
references:
- https://www.jenkins.io/doc/developer/security/csrf-protection/
source: independent security review
license: MIT
languages: [java]
mode: search
severity: HIGH
37 changes: 37 additions & 0 deletions testcases/java/statechange-without-requirepost-neg.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.Cause;
import hudson.model.Executor;
import hudson.model.Result;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.interceptor.RequirePOST;

public class StateChangeNeg implements Action {
private final AbstractProject<?, ?> project;

public StateChangeNeg(AbstractProject<?, ?> project) {
this.project = project;
}

public String getUrlName() {
return "accelerated";
}

// fixed: @RequirePOST present -> GET rejected, crumb enforced
@RequirePOST
public void doBuild(final StaplerRequest request, final StaplerResponse response) {
project.scheduleBuild2(0, new Cause.UserIdCause(), new Action[0]);
Executor executor = getExecutor();
if (executor != null) {
executor.interrupt(Result.ABORTED);
}
Jenkins.getInstance().getQueue().setSorter(null);
response.sendRedirect(request.getContextPath() + '/' + project.getUrl());
}

private Executor getExecutor() {
return null;
}
}
35 changes: 35 additions & 0 deletions testcases/java/statechange-without-requirepost-pos.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.Cause;
import hudson.model.Executor;
import hudson.model.Result;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;

public class StateChangePos implements Action {
private final AbstractProject<?, ?> project;

public StateChangePos(AbstractProject<?, ?> project) {
this.project = project;
}

public String getUrlName() {
return "accelerated";
}

// vulnerable: no @RequirePOST -> GET-reachable state changes
public void doBuild(final StaplerRequest request, final StaplerResponse response) {
project.scheduleBuild2(0, new Cause.UserIdCause(), new Action[0]);
Executor executor = getExecutor();
if (executor != null) {
executor.interrupt(Result.ABORTED);
}
Jenkins.getInstance().getQueue().setSorter(null);
response.sendRedirect(request.getContextPath() + '/' + project.getUrl());
}

private Executor getExecutor() {
return null;
}
}