VYPR
Critical severity9.8NVD Advisory· Published May 26, 2026· Updated May 26, 2026

CVE-2026-44668

CVE-2026-44668

Description

FACTION is a PenTesting Report Generation and Collaboration Framework. Prior to 1.8.3, AccessControlInterceptor, the authentication gate for all Struts2 actions, unconditionally calls invocation.invoke() without checking for a valid session. Four action methods in BoilerPlateConfig perform no local session check either, allowing an unauthenticated attacker to read, overwrite, deactivate, and permanently delete any boilerplate template in the system. This vulnerability is fixed in 1.8.3.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Unauthenticated attacker can read, overwrite, deactivate, or delete boilerplate templates in FACTION prior to 1.8.3 via missing session checks.

Vulnerability

The vulnerability is in FACTION, a PenTesting Report Generation and Collaboration Framework, prior to version 1.8.3. The AccessControlInterceptor, which is the authentication gate for all Struts2 actions, unconditionally calls invocation.invoke() without checking for a valid session [1]. Additionally, four action methods in BoilerPlateConfig (searchTemplateDetail, globalSaveTemplate, tempActive, tempDelete) perform no local session check, allowing unauthenticated access to endpoints that read, overwrite, deactivate, or delete boilerplate templates [1].

Exploitation

An attacker needs no authentication and only network access to the FACTION instance. Template IDs are sequential integers, allowing enumeration. The attacker can send direct HTTP requests to the endpoints: GET /portal/tempSearchDetail.action?tmpId={id} to read templates, POST /portal/globalSave.action to overwrite templates, POST /portal/tempActive.action to deactivate templates, and POST /portal/tempDelete.action to permanently delete templates [1].

Impact

An unauthenticated attacker can read the content of any boilerplate template (including private ones), overwrite template text, deactivate templates (making them unavailable), or permanently delete templates. This compromises the confidentiality, integrity, and availability of template data. The attacker gains full control over all boilerplate templates in the system [1].

Mitigation

The vulnerability is fixed in FACTION version 1.8.3, released on the same date as the advisory [2]. In the fix, the AccessControlInterceptor now blocks unauthenticated requests to all non-root namespaces, and the four BoilerPlateConfig actions require an authenticated session, with template detail lookups scoped to the requesting user's own templates or global templates [2]. Users should upgrade to version 1.8.3 immediately. No workaround is mentioned for earlier versions.

AI Insight generated on May 26, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

2
825cc330ad14

Backlog release (#130)

https://github.com/factionsecurity/factionJoshMay 4, 2026Fixed in 1.8.3via llm-release-walk
10 files changed · +246 20
  • src/com/fuse/actions/admin/CMS.java+38 1 modified
    @@ -1,8 +1,10 @@
     package com.fuse.actions.admin;
     
    +import java.io.ByteArrayInputStream;
     import java.io.File;
     import java.io.FileInputStream;
     import java.io.IOException;
    +import java.io.InputStream;
     import java.util.ArrayList;
     import java.util.Arrays;
     import java.util.List;
    @@ -66,6 +68,8 @@ public class CMS extends FSActionSupport {
     	private String reportExtension;
     	private List<List<String>> reportSections = new ArrayList<>();
     	private String reportSection;
    +	private InputStream templateStream;
    +	private String downloadFilename;
     
     	@Action(value = "cms", results = {
     			@Result(name = "templateUpload", location = "/WEB-INF/jsp/cms/TemplateUpload.jsp"),
    @@ -226,6 +230,32 @@ public String execute() throws IOException {
     
     		return SUCCESS;
     	}
    +	@Action(value = "downloadTemplate", results = {
    +			@Result(
    +					name = "download",
    +					type = "stream",
    +					params = {
    +							"inputName", "templateStream",
    +							"contentType", "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    +							"bufferSize", "1024",
    +							"contentDisposition", "attachment;filename=\"${downloadFilename}\""
    +					}
    +			)
    +	})
    +	public String downloadTemplate() {
    +		if (!(this.isAcadmin() || this.isAcengagement() || this.isAcassessor()))
    +			return LOGIN;
    +		if (id == null) return ERROR;
    +		selectedTemplate = (ReportTemplates) em.createQuery("from ReportTemplates where id = :id")
    +				.setParameter("id", id).getResultList().stream().findFirst().orElse(null);
    +		if (selectedTemplate == null || !selectedTemplate.getSaveInDB()
    +				|| selectedTemplate.getBase64EncodedTemplate() == null)
    +			return ERROR;
    +		templateStream = selectedTemplate.getTemplate();
    +		downloadFilename = selectedTemplate.getFilename();
    +		return "download";
    +	}
    +
     	@Action(value = "checkReportValues", results = {
     			@Result( name="ok", type = "httpheader", params = { "status", "202"} ),
     			@Result( name="none",type = "httpheader", params = { "status", "404"} ),
    @@ -572,6 +602,13 @@ public List<List<String>> getReportSections() {
     	public void setReportSection(String reportSection) {
     		this.reportSection = reportSection;
     	}
    -	
    +
    +	public InputStream getTemplateStream() {
    +		return templateStream;
    +	}
    +
    +	public String getDownloadFilename() {
    +		return downloadFilename;
    +	}
     
     }
    
  • src/com/fuse/actions/assessment/AssessmentView.java+98 5 modified
    @@ -1,7 +1,15 @@
     package com.fuse.actions.assessment;
     
     import java.io.ByteArrayInputStream;
    +import java.io.File;
    +import java.io.FileInputStream;
     import java.io.InputStream;
    +
    +import org.apache.commons.codec.binary.Base64;
    +
    +import com.fuse.dao.FinalReport;
    +import com.fuse.dao.HibHelper;
    +
     import java.net.URLEncoder;
     import java.nio.charset.StandardCharsets;
     import java.time.LocalDateTime;
    @@ -17,6 +25,7 @@
     import java.util.LinkedHashMap;
     import java.util.List;
     import java.util.Map;
    +import java.util.UUID;
     import java.util.stream.Collectors;
     
     import javax.mail.MessagingException;
    @@ -93,6 +102,9 @@ public class AssessmentView extends FSActionSupport {
     	private List<Status> statuses = new ArrayList<>();
     	private Long status;
     	private Boolean hasTemplate = false;
    +	private File uploadReport;
    +	private String uploadReportContentType;
    +	private String uploadReportFilename;
     	
     
     	@Action(value = "Assessment", 
    @@ -617,6 +629,80 @@ public String SendToPR() {
     
     	}
     
    +	@Action(value = "UploadFinalReport")
    +	public String uploadReport() throws Exception {
    +		if (!(this.isAcassessor() || this.isAcmanager()))
    +			return LOGIN;
    +
    +		User user = this.getSessionUser();
    +		Long asmtId = SessionAsmtId();
    +
    +		Assessment asmt;
    +		if (this.isAcmanager()) {
    +			asmt = AssessmentQueries.getAssessmentById(em, asmtId);
    +		} else {
    +			asmt = AssessmentQueries.getAssessmentByUserId(em, user.getId(), asmtId, AssessmentQueries.All);
    +		}
    +
    +		if (!this.testToken(false))
    +			return this.ERRORJSON;
    +
    +		if (asmt == null || asmt.getCompleted() != null) {
    +			this._message = "Assessment not found or already finalized.";
    +			return this.ERRORJSON;
    +		}
    +
    +		if (!this.isAcmanager() && asmt.getAssessor().stream().noneMatch(u -> u.getId() == user.getId())) {
    +			this._message = "You are not an assessor on this assessment.";
    +			return this.ERRORJSON;
    +		}
    +
    +		if (uploadReport == null) {
    +			this._message = "No file uploaded.";
    +			return this.ERRORJSON;
    +		}
    +
    +		String ct = uploadReportContentType == null ? "" : uploadReportContentType.toLowerCase();
    +		String fn = uploadReportFilename == null ? "" : uploadReportFilename.toLowerCase();
    +		boolean isPdf = ct.contains("pdf") || fn.endsWith(".pdf");
    +		boolean isDocx = ct.contains("wordprocessingml") || fn.endsWith(".docx");
    +
    +		if (!isPdf && !isDocx) {
    +			this._message = "Only .docx and .pdf files are allowed.";
    +			return this.ERRORJSON;
    +		}
    +
    +		byte[] fileBytes = new byte[(int) uploadReport.length()];
    +		try (FileInputStream fis = new FileInputStream(uploadReport)) {
    +			fis.read(fileBytes);
    +		}
    +		String b64 = Base64.encodeBase64String(fileBytes);
    +
    +		HibHelper.getInstance().preJoin();
    +		em.joinTransaction();
    +
    +		if (asmt.getFinalReport() == null) {
    +			FinalReport fr = new FinalReport();
    +			fr.setRetest(false);
    +			fr.setFilename(UUID.randomUUID().toString());
    +			fr.setBase64EncodedPdf(b64);
    +			fr.setGentime(new Date());
    +			fr.setFileType(isPdf ? "pdf" : "docx");
    +			em.persist(fr);
    +			asmt.setFinalReport(fr);
    +		} else {
    +			asmt.getFinalReport().setBase64EncodedPdf(b64);
    +			asmt.getFinalReport().setFileType(isPdf ? "pdf" : "docx");
    +			asmt.getFinalReport().setGentime(new Date());
    +		}
    +
    +		em.persist(asmt);
    +		HibHelper.getInstance().commit();
    +
    +		AuditLog.audit(this, "Report uploaded for assessment " + asmt.getName(), AuditLog.CompAssessment, false);
    +		return this.SUCCESSJSON;
    +	}
    +
     	private Map<String, String> jsonSuccessMessage;
     
     	public Map<String, String> getJsonSuccessMessage() {
    @@ -960,10 +1046,17 @@ public List<Status> getStatuses(){
     	public void setStatus(Long status) {
     		this.status = status;
     	}
    -	
    -	
    -	
    -	   
    -	
    +
    +	public void setUploadReport(File uploadReport) {
    +		this.uploadReport = uploadReport;
    +	}
    +
    +	public void setUploadReportContentType(String uploadReportContentType) {
    +		this.uploadReportContentType = uploadReportContentType;
    +	}
    +
    +	public void setUploadReportFilename(String uploadReportFilename) {
    +		this.uploadReportFilename = uploadReportFilename;
    +	}
     
     }
    
  • src/com/fuse/actions/assessment/BoilerPlateConfig.java+10 1 modified
    @@ -60,6 +60,7 @@ public String searchTemplate() {
     
    
     	@Action(value = "tempDelete")
    
     	public String tempDelete() {
    
    +		if (this.getSessionUser() == null) return LOGIN;
    
     		BoilerPlate bp = (BoilerPlate) em.createQuery("from BoilerPlate where id = :id")
    
     				.setParameter("id", this.tmpId).getResultList()
    
     				.stream().findFirst().orElse(null);
    
    @@ -72,6 +73,7 @@ public String tempDelete() {
     	}
    
     	@Action(value = "tempActive")
    
     	public String tempActive() {
    
    +		if (this.getSessionUser() == null) return LOGIN;
    
     		BoilerPlate bp = (BoilerPlate) em.createQuery("from BoilerPlate where id = :id")
    
     				.setParameter("id", this.tmpId).getResultList()
    
     				.stream().findFirst().orElse(null);
    
    @@ -87,7 +89,13 @@ public String tempActive() {
     	@Action(value = "tempSearchDetail", results = {
    
     			@Result(name = "tempSearchDetailJson", location = "/WEB-INF/jsp/assessment/tempSearchDetailJSON.jsp") })
    
     	public String searchTemplateDetail() {
    
    -		BoilerPlate bp = em.find(BoilerPlate.class, this.tmpId);
    
    +		if (this.getSessionUser() == null) return LOGIN;
    
    +		BoilerPlate bp = (BoilerPlate) em
    
    +				.createQuery("from BoilerPlate where id = :id and (global = true or user = :user)")
    
    +				.setParameter("id", this.tmpId)
    
    +				.setParameter("user", this.getSessionUser())
    
    +				.getResultList().stream().findFirst().orElse(null);
    
    +		if (bp == null) return this.ERRORJSON;
    
     		boilers = new ArrayList();
    
     		boilers.add(bp);
    
     
    
    @@ -96,6 +104,7 @@ public String searchTemplateDetail() {
     	@Action(value = "globalSave", results = {
    
     			@Result(name = "tempSearchJson", location = "/WEB-INF/jsp/assessment/tempSearchJSON.jsp") })
    
     	public String globalSaveTemplate() {
    
    +		if (this.getSessionUser() == null) return LOGIN;
    
     		BoilerPlate bp = (BoilerPlate) em
    
     				.createQuery(
    
     						"from BoilerPlate where id=:id and global=true")
    
    
  • src/com/fuse/servlets/fileUpload.java+18 6 modified
    @@ -15,12 +15,15 @@
     import javax.servlet.http.HttpServletResponse;
     import javax.servlet.http.Part;
     
    +import java.net.URLEncoder;
    +
     import org.apache.commons.fileupload.servlet.ServletFileUpload;
     import org.apache.commons.lang3.StringEscapeUtils;
     
     import com.fuse.dao.Files;
     import com.fuse.dao.HibHelper;
     import com.fuse.dao.User;
    +import com.fuse.utils.FSUtils;
     
     /**
      * Servlet implementation class fileUpload
    @@ -105,7 +108,12 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
     			Files f = new Files();
     			f.setUuid(uuid);
     			f.setContentType(filePart.getContentType());
    -			f.setName(getFileName(filePart));
    +			String fileName = getFileName(filePart);
    +			if (!isSafeFileName(fileName)) {
    +				response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid file name");
    +				return;
    +			}
    +			f.setName(fileName);
     			byte[] bytes = new byte[(int) filePart.getSize()];
     			filePart.getInputStream().read(bytes);
     			f.setRealFile(bytes);
    @@ -155,14 +163,14 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
     						+ "' class='file-preview-image' />\"], ";
     			} else if (f.getContentType().contains("text")) {
     				// try {
    -				json += "\"initialPreview\": [\"<pre class='file-preview-text' title='" + f.getName()
    +				json += "\"initialPreview\": [\"<pre class='file-preview-text' title='" + StringEscapeUtils.escapeHtml4(f.getName())
     						+ "' style='width:100%;height:158px;' >"
     						+ StringEscapeUtils.escapeJson(new String(f.getRealFile())) + "</pre>";
     				json += "\"], ";
     
     			} else {
     				json += "\"initialPreview\": [\"<object class='file-object'  type='" + f.getContentType()
    -						+ "' height='160px' width='160px'>" + "<param name='movie' value='" + f.getName() + "'>"
    +						+ "' height='160px' width='160px'>" + "<param name='movie' value='" + StringEscapeUtils.escapeHtml4(f.getName()) + "'>"
     						+ "<param name='controller' value='true'>" + "<param name='allowFullScreen' value='true'>"
     						+ "<param name='allowScriptAccess' value='always'>" + "<param name='autoPlay' value='false'>"
     						+ "<param name='autoStart' value='false'>" + "<param name='quality' value='high'>"
    @@ -175,12 +183,12 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
     				json += "\"], ";
     			}
     			if (apid != null && !apid.equals("")) {
    -				json += "\"initialPreviewConfig\" : [" + "{ " + "\"caption\": \"" + f.getName() + "\", "
    +				json += "\"initialPreviewConfig\" : [" + "{ " + "\"caption\": \"" + StringEscapeUtils.escapeJson(f.getName()) + "\", "
     						+ "\"width\" : \"100px\", " + "\"url\" : \"../service/fileUpload?delid=" + f.getUuid()
    -						+ "&apid=" + f.getEntityId() + "&name=" + f.getName() + "\", "
    +						+ "&apid=" + f.getEntityId() + "&name=" + URLEncoder.encode(f.getName(), "UTF-8") + "\", "
     						+ "\"downloadUrl\": \"../service/fileUpload?id=" + f.getUuid() + "\",\"key\" : 1}" + "]" + "}";
     			} else {
    -				json += "\"initialPreviewConfig\" : [{ \"caption\": \"" + f.getName()
    +				json += "\"initialPreviewConfig\" : [{ \"caption\": \"" + StringEscapeUtils.escapeJson(f.getName())
     						+ "\", \"width\" : \"100px\", \"url\" : \"../service/fileUpload?name=" + uuid + "\", "
     						+ "\"downloadUrl\": \"../service/fileUpload?id=" + f.getUuid() + "\",\"key\" : 1}]}";
     			}
    @@ -242,6 +250,10 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
     
     	}
     
    +	private boolean isSafeFileName(String name) {
    +		return name != null && !name.isEmpty() && name.matches("[\\w\\s.\\-]+");
    +	}
    +
     	private String getFileName(Part p) {
     		String header = p.getHeader("Content-Disposition");
     		String[] headers = header.split(";");
    
  • src/com/fuse/tasks/ReportGenThread.java+2 3 modified
    @@ -33,14 +33,13 @@ public class ReportGenThread implements Runnable{
     	public ReportGenThread(String host, Assessment asmt, List<User> notifiers, boolean retest){
    
     		this.host = host;
    
     		this.asmt = asmt;
    
    -		this.notifiers = notifiers;
    
    +		this.notifiers = new ArrayList<>(notifiers);
    
     		this.isRetest=retest;
    
     	}
    
     	public ReportGenThread(String host, Assessment asmt, List<User> notifiers){
    
     		this.host = host;
    
     		this.asmt = asmt;
    
    -		this.notifiers = notifiers;
    
    -		
    
    +		this.notifiers = new ArrayList<>(notifiers);
    
     	}
    
     	public ReportGenThread(String host, Assessment asmt, User notifiers){
    
     		this.host = host;
    
    
  • src/com/fuse/utils/AccessControlInterceptor.java+8 2 modified
    @@ -19,14 +19,20 @@ public String intercept(ActionInvocation invocation) throws Exception {
     		
     		User user = (User) ServletActionContext.getRequest().getSession(true).getAttribute("user");
     		
    -		if(user != null){
    +		if (user != null) {
     			ActionContext.getContext().put("user", user);
     			ActionContext.getContext().put("isAdmin", user.getPermissions().isAdmin());
     			ActionContext.getContext().put("isManager", user.getPermissions().isManager());
     			ActionContext.getContext().put("isAssessor", user.getPermissions().isAssessor());
     			ActionContext.getContext().put("isEngagement", user.getPermissions().isEngagement());
     			ActionContext.getContext().put("isRemediation", user.getPermissions().isRemediation());
    -			
    +		} else {
    +			// Allow unauthenticated access only to the root namespace (login, reset, setup).
    +			// All other namespaces require an active session.
    +			String namespace = invocation.getProxy().getNamespace();
    +			if (!"/".equals(namespace)) {
    +				return "login";
    +			}
     		}
     		return invocation.invoke();
     	}
    
  • WebContent/dist/js/overview.js+1 1 modified
  • WebContent/src/assessment/overview.js+41 0 modified
    @@ -405,6 +405,47 @@ $(function() {
     
     		});
     	});
    +	$("#uploadReportBtn").click(function() {
    +		$('#uploadReportError').hide();
    +		$('#uploadReportFile').val('');
    +		$('#uploadReportModal').modal('show');
    +	});
    +
    +	$("#doUploadReport").click(function() {
    +		var file = $('#uploadReportFile')[0].files[0];
    +		if (!file) {
    +			$('#uploadReportError').text('Please select a file.').show();
    +			return;
    +		}
    +		var ext = file.name.split('.').pop().toLowerCase();
    +		if (ext !== 'docx' && ext !== 'pdf') {
    +			$('#uploadReportError').text('Only .docx and .pdf files are allowed.').show();
    +			return;
    +		}
    +		var formData = new FormData();
    +		formData.append('uploadReport', file);
    +		formData.append('_token', global._token);
    +		$('#doUploadReport').prop('disabled', true).text('Uploading...');
    +		$.ajax({
    +			url: 'UploadFinalReport',
    +			type: 'POST',
    +			data: formData,
    +			processData: false,
    +			contentType: false,
    +			success: function(resp) {
    +				global._token = resp.token;
    +				$('#uploadReportModal').modal('hide');
    +				location.reload();
    +			},
    +			error: function(xhr) {
    +				var msg = 'Upload failed.';
    +				try { msg = JSON.parse(xhr.responseText).message || msg; } catch(e) {}
    +				$('#uploadReportError').text(msg).show();
    +				$('#doUploadReport').prop('disabled', false).text('Upload');
    +			}
    +		});
    +	});
    +
     	$("#finalize").click(function() {
     		$(".content").loading({ overlay: true, base: 0.3 });
     		$.confirm({
    
  • WebContent/WEB-INF/jsp/assessment/Finalize.jsp+29 0 modified
    @@ -61,6 +61,9 @@
     				</span>
     		</div>
     		</s:if>
    +		  <div style="margin-top:10px;">
    +			<bs:button color="info" size="md" colsize="3" text="Upload Report" id="uploadReportBtn"></bs:button>
    +		  </div>
               
            </div>
          </li>
    @@ -154,3 +157,29 @@
     </bs:mco>
     </bs:row>
     
    +<!-- Upload Report Modal -->
    +<div class="modal fade" id="uploadReportModal" tabindex="-1" role="dialog">
    +  <div class="modal-dialog" role="document">
    +    <div class="modal-content" style="background-color:#192338; color:#fff;">
    +      <div class="modal-header" style="border-bottom-color:#0f1a2b;">
    +        <button type="button" class="close" style="color:#fff; opacity:0.8;" data-dismiss="modal">&times;</button>
    +        <h4 class="modal-title">Upload Report</h4>
    +      </div>
    +      <div class="modal-body">
    +        <p>Upload a <strong>.docx</strong> or <strong>.pdf</strong> to replace the current report.</p>
    +        <form id="uploadReportForm" enctype="multipart/form-data">
    +          <div class="form-group">
    +		  <input type="file" name="uploadReport" id="uploadReportFile" accept=".docx,.pdf" required="" class="form-control" style="background-color: #192338;color:#fff;border-color:#0f1a2b;">
    +          </div>
    +          <div id="uploadReportError" class="alert alert-danger" style="display:none;"></div>
    +        </form>
    +      </div>
    +      <div class="modal-footer" style="border-top-color:#0f1a2b;">
    +        <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
    +        <button type="button" class="btn btn-primary" id="doUploadReport">Upload</button>
    +      </div>
    +    </div>
    +  </div>
    +</div>
    +
    +
    
  • WebContent/WEB-INF/jsp/cms/TemplateUpload.jsp+1 1 modified
    @@ -71,7 +71,7 @@
     	
    
     	<bs:mco colsize="12">
    
     		<br>
    
    -		<span><i> Currently Uploaded to : <br>${selectedTemplate.filename }</i></span>
    
    +		<span><i> Download Template: <br><s:if test="selectedTemplate.filename != null && !selectedTemplate.filename.isEmpty() && selectedTemplate.saveInDB"><a href="downloadTemplate?id=${selectedTemplate.id}">${selectedTemplate.filename}</a></s:if><s:else>${selectedTemplate.filename}</s:else></i></span>
    
     	</bs:mco>
    
     	
    
     </form>
    
    
90b341744fb6

[maven-release-plugin] prepare release 1.8.3

https://github.com/factionsecurity/factionJoshMay 4, 2026Fixed in 1.8.3via release-tag
1 file changed · +2 2
  • pom.xml+2 2 modified
    @@ -2,15 +2,15 @@
     	<modelVersion>4.0.0</modelVersion>
     	<groupId>org.faction</groupId>
     	<artifactId>faction</artifactId>
    -	<version>1.8.3-SNAPSHOT</version>
    +	<version>1.8.3</version>
     	<packaging>war</packaging>
     	<name>Faction</name>
     	<scm>
     		<url>https://github.com/factionsecurity/faction.git</url>
     		<connection>scm:git:https://github.com/factionsecurity/faction.git</connection>
     		<developerConnection>
     			scm:git:https://github.com/factionsecurity/faction.git</developerConnection>
    -		<tag>1.7.5</tag>
    +		<tag>1.8.3</tag>
     	</scm>
     	<properties>
     		<aws.java.sdk.version>2.18.16</aws.java.sdk.version>
    

Vulnerability mechanics

Root cause

"Missing authentication check in AccessControlInterceptor and four BoilerPlateConfig action methods allows unauthenticated access to CRUD operations on boilerplate templates."

Attack vector

An unauthenticated attacker sends HTTP requests directly to the four vulnerable Struts2 action endpoints — `tempSearchDetail.action`, `globalSave.action`, `tempActive.action`, and `tempDelete.action` — without providing any session cookie or credentials [ref_id=1]. Because `AccessControlInterceptor` does not reject unauthenticated requests and the action methods lack their own session checks, the server processes the request and performs the database operation [ref_id=1]. Template IDs are sequential integers, so an attacker can enumerate IDs to read, overwrite, deactivate, or delete any boilerplate template in the system [ref_id=1].

Affected code

The vulnerability spans two files. `AccessControlInterceptor.java` unconditionally calls `invocation.invoke()` without checking for a valid session, allowing unauthenticated requests to reach any action method. Four action methods in `BoilerPlateConfig.java` — `searchTemplateDetail()`, `globalSaveTemplate()`, `tempActive()`, and `tempDelete()` — perform no local session check either, so they execute without authentication [ref_id=1].

What the fix does

The patch adds a session check to `AccessControlInterceptor.java` that returns `"login"` when no user is in the session and the request namespace is not the root namespace, blocking unauthenticated requests before they reach any action method [patch_id=2566846]. As defense in depth, each of the four `BoilerPlateConfig` methods now also checks `if (this.getSessionUser() == null) return LOGIN;`, and `searchTemplateDetail()` additionally scopes its query to templates owned by the session user or marked global, preventing cross-user access [patch_id=2566846][ref_id=1].

Preconditions

  • authNo authentication required; attacker does not need a session cookie or credentials
  • networkNetwork access to the Faction web application on any exposed port
  • inputAt least one boilerplate template must exist in the database (for read/overwrite/deactivate/delete to have an effect)

Reproduction

The advisory includes a full PoC [ref_id=1]. After starting Faction via Docker and creating an admin account and a boilerplate template, the attacker issues unauthenticated requests: `curl "http://localhost:8080/portal/tempSearchDetail.action?tmpId=20"` to read a template, `curl -X POST "http://localhost:8080/portal/globalSave.action" -d "tmpId=20&summary=INJECTED+BY+ATTACKER&active=true"` to overwrite it, `curl -X POST "http://localhost:8080/portal/tempActive.action" -d "tmpId=20&active=false"` to deactivate it, and `curl -X POST "http://localhost:8080/portal/tempDelete.action" -d "tmpId=20"` to permanently delete it [ref_id=1].

Generated on May 26, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

2

News mentions

0

No linked articles in our index yet.