What's new

This page lists all of the changes added in the 2025.11.x release.

2025.11.4 release

This section lists all of the changes added in patch 4 of the 2025.11 release.

New Jutro version

InsuranceSuite application affected: N/A

Digital reference application affected: All Digital reference applications

Digital reference applications now use Jutro patch version 10.12.6. This brings enhanced security to the Jutro Design System. For more information, see What's new in Jutro patch 10.12.6.

Response header configuration changes

InsuranceSuite application affected: PolicyCenter, ClaimCenter

Digital reference application affected: CustomerEngage Account Management, CustomerEngage Account Management for ClaimCenter, ProducerEngage, ProducerEngage for ClaimCenter, ServiceRepEngage

Guidewire has modified the configuration of certain response headers so that file uploads are better sanitized. There are steps that you can take to have better sanitized file uploads and need to manually add the changes to your own files. The changes need to be added to the following files:
  • gsrc/edge/capabilities/gateway/document/DocumentUploadHandler.gs
  • gsrc/edge/capabilities/policy/document/PolicyDocumentUploadHandler.gs
  • gsrc/edge/capabilities/granite_on_cloud-servicerequest/document/ServiceRequestFileUploadHandler.gs
  • gsrc/edge/security/fileupload/FileUploadUtil.gs
  • gsrc/edge/capabilities/claim/document/ClaimDocumentUploadHandler.gs‎

Changes to the document upload handler

The following sections show the original and updated code. Bold lines in each code block indicate where the changes were made for this fix. Make these changes in DocumentUploadHandler.gs:

The following codeblock contains the original file content. Highlighted text indicates content removed or changed in the updated file:
package edge.capabilities.gateway.document

uses edge.jsonrpc.AbstractRpcHandler
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.annotation.JsonRpcRunAsInternalGWUser
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.document.dto.DocumentBaseDTO
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses org.apache.commons.fileupload2.core.FileItem
uses gw.plugin.document.IDocumentContentSource
uses gw.plugin.Plugins
uses edge.capabilities.gateway.document.dto.DocumentDTO
uses edge.di.annotations.InjectableNode
uses edge.PlatformSupport.Bundle
uses edge.security.fileupload.IFileUploadPlugin
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.security.authorization.exception.AuthorizationException
uses edge.security.fileupload.exception.IllegalContentTypeException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey

class DocumentUploadHandler extends AbstractRpcHandler {

  var _documentPlugin : IDocumentPlugin
  private var _documentSessionPlugin : IDocumentSessionPlugin
  private var _fileUploadPlugin : IFileUploadPlugin

  @InjectableNode
  @Param("documentSessionPlugin", "Plugin used to deal with document sessions")
  @Param("fileUploadPlugin", "Plugin used to verify file upload validity")
  construct(aDocumentPlugin : IDocumentPlugin,
  documentSessionPlugin : IDocumentSessionPlugin,
  fileUploadPlugin : IFileUploadPlugin){
    this._documentPlugin = aDocumentPlugin
    this._documentSessionPlugin = documentSessionPlugin
    this._fileUploadPlugin = fileUploadPlugin
  }

  /**
   * Uploads a document
   *
   * <dl>
   *   <dt>Calls:</dt>
   * <dd> <code>IDocumentPlugin#createDocument(DocumentDTO)</code> - To create a document</dd>
   * <dd> <code>IDocumentPlugin#toDTO(Document)</code> - To return DTO with document </dd>
   * <dd> <code>IDocumentContentSource#addDocument(InputStream, Document)</code> - To add Document</dd>
   * <dt>Throws:</dt>
   *  <dd><code>IllegalArgumentException</code> - If claim number associated with the document is null or empty</dd>
   *  <dd><code>EntityNotFoundException</code> - If no claim is found</dd>
   *  <dd><code>AuthorizationException</code> - If the portal user has no access to the claim</dd>
   * </dl>
   * @param documentDTO
   * @param fileItem
   * @return document metadata response
   */
  @JsonRpcRunAsInternalGWUser
  @JsonRpcMethod
  @ApidocMethodDescription("Uploads a document. NOTE: Given a list of file items, it expects that the element " +
      "at [0] is the document metadata; at [1] is the item to be uploaded")
  @ApidocAvailableSince("5.0")
  function upload(documentDto: DocumentDTO, documentFile: FileItem): DocumentBaseDTO {

    try {
      // validate upload token is still valid
      _documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
    } catch (ex : JsonRpcSecurityException){
      throw new AuthorizationException(){:Message = "Unauthorized portal access"}
    }

    if(documentDto.Name!=documentFile.Name){
      throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
    }

    if(documentDto.MimeType!=documentFile.ContentType) {
      throw new IllegalContentTypeException(DisplayKey.get("Document.upload.contentType.error", documentFile.ContentType))
    }

    if (!_fileUploadPlugin.canUploadContentType(documentFile)) {
      throw new IllegalContentTypeException("Cannot upload files of content type: " + documentFile.ContentType)
    }

    /** Some browsers (or fileupload library on some browsers) do not populate this field). */
    if (documentDto.MimeType == null) {
      documentDto.MimeType = documentFile.ContentType
    }

    final var doc = Bundle.resolveInTransaction( \ bundle -> {
      try {
        final var _doc = _documentPlugin.createDocument(documentDto)
        // add the document to the dms
        Plugins.get(IDocumentContentSource).addDocument(documentFile.InputStream, _doc)
        return _doc
      } finally {
        documentFile.InputStream.close()
      }
    })
    return _documentPlugin.toDTO(doc)
  }
}
The following codeblock contains the updated file content. Highlighted text indicates updates:
package edge.capabilities.gateway.document

uses edge.jsonrpc.AbstractRpcHandler
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.annotation.JsonRpcRunAsInternalGWUser
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.document.dto.DocumentBaseDTO
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses org.apache.commons.fileupload2.core.FileItem
uses gw.plugin.document.IDocumentContentSource
uses gw.plugin.Plugins
uses edge.capabilities.gateway.document.dto.DocumentDTO
uses edge.di.annotations.InjectableNode
uses edge.PlatformSupport.Bundle
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.security.authorization.exception.AuthorizationException
uses edge.security.fileupload.exception.IllegalContentTypeException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey
uses org.apache.tika.Tika
uses gw.api.util.Logger
uses edge.security.fileupload.DefaultFileUploadPlugin
uses edge.security.fileupload.FileUploadUtil

class DocumentUploadHandler extends AbstractRpcHandler {

  private static final var LOG = Logger.forCategory(DocumentUploadHandler.Type.QName)
  private static final var TIKA = new Tika()
    var _documentPlugin : IDocumentPlugin
    private var _documentSessionPlugin : IDocumentSessionPlugin
    static final var MIME_PNG = "image/png"
    static final var MIME_JPEG = "image/jpeg"
    static final var MIME_PDF = "application/pdf"
  
    @InjectableNode
    @Param("documentSessionPlugin", "Plugin used to deal with document sessions")
    construct(aDocumentPlugin : IDocumentPlugin,
 documentSessionPlugin : IDocumentSessionPlugin){
      this._documentPlugin = aDocumentPlugin
      this._documentSessionPlugin = documentSessionPlugin
    }
  
    /**
     * Uploads a document
     *
     * <dl>
     *   <dt>Calls:</dt>
     * <dd> <code>IDocumentPlugin#createDocument(DocumentDTO)</code> - To create a document</dd>
     * <dd> <code>IDocumentPlugin#toDTO(Document)</code> - To return DTO with document </dd>
     * <dd> <code>IDocumentContentSource#addDocument(InputStream, Document)</code> - To add Document</dd>
     * <dt>Throws:</dt>
     *  <dd><code>IllegalArgumentException</code> - If claim number associated with the document is null or empty</dd>
     *  <dd><code>EntityNotFoundException</code> - If no claim is found</dd>
     *  <dd><code>AuthorizationException</code> - If the portal user has no access to the claim</dd>
     * </dl>
     * @param documentDTO
     * @param fileItem
     * @return document metadata response
     */
    @JsonRpcRunAsInternalGWUser
    @JsonRpcMethod
    @ApidocMethodDescription("Uploads a document. NOTE: Given a list of file items, it expects that the element " +
        "at [0] is the document metadata; at [1] is the item to be uploaded")
    @ApidocAvailableSince("5.0")
    function upload(documentDto: DocumentDTO, documentFile: FileItem): DocumentBaseDTO {
  
      try {
        // validate upload token is still valid
        _documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
      } catch (ex : JsonRpcSecurityException){
        throw new AuthorizationException(){:Message = "Unauthorized portal access"}
      }
  
      if (documentDto.Name != documentFile.Name) {
        throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
      }

      FileUploadUtil.assertSafeFileName(documentDto.Name)
      FileUploadUtil.assertSafeFileName(documentFile.Name)
  
      if (documentDto.MimeType == null) {
        documentDto.MimeType = documentFile.ContentType
      }
  
      FileUploadUtil.enforceMaxUploadSize(documentFile)
  
      var bytes = FileUploadUtil.readAllBytesAndCloseWithLimit(documentFile, FileUploadUtil.getMaxUploadBytes())
  
      // Pass filename so we can disambiguate OOXML when Tika reports application/zip.
      var detectedType = FileUploadUtil.normalizeMimeType(TIKA.detect(bytes), documentFile.Name)
  
      FileUploadUtil.enforcePdfNameConsistency(documentFile.Name, detectedType, MIME_PDF)
      FileUploadUtil.assertExtensionMatchesType(documentFile.Name, detectedType)

      if (!isAllowedDetectedType(detectedType)) {
        throw new IllegalContentTypeException("Cannot upload files of detected content type: " + detectedType)
      }
  
      FileUploadUtil.validateContent(detectedType, bytes, MIME_PDF, MIME_PNG, MIME_JPEG)
  
      documentDto.MimeType = detectedType
  
      final var doc = Bundle.resolveInTransaction(\ bundle -> {
        final var _doc = _documentPlugin.createDocument(documentDto)
        var uploadInputStream = new java.io.ByteArrayInputStream(bytes)
        try {
          Plugins.get(IDocumentContentSource).addDocument(uploadInputStream, _doc)
        } finally {
          uploadInputStream.close()
        }
        return _doc
      })
  
      return _documentPlugin.toDTO(doc)
    }
  
    private static function isAllowedDetectedType(detectedType : String) : boolean {
      return new DefaultFileUploadPlugin().canUploadContentType(detectedType)
  }
}

Changes to the policy document upload handler

The following sections show the original and updated code. Bold lines in each code block indicate where the changes were made for this fix. Make these changes in PolicyDocumentUploadHandler.gs:

The following codeblock contains the original file content. Highlighted text indicates content removed or changed in the updated file:
package edge.capabilities.policy.document

uses edge.jsonrpc.AbstractRpcHandler
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses org.apache.commons.fileupload2.core.FileItem
uses edge.di.annotations.InjectableNode
uses gw.api.webservice.exception.BadIdentifierException
uses edge.capabilities.policy.auth.IPolicyAccessPlugin
uses edge.security.authorization.exception.NoAuthorityException
uses edge.capabilities.policy.document.dto.PolicyDocumentDTO
uses gw.plugin.Plugins
uses gw.plugin.document.IDocumentContentSource
uses edge.capabilities.policy.document.dto.PolicyDocumentUploadDTO
uses edge.PlatformSupport.Bundle
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.security.authorization.exception.AuthorizationException
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.policy.util.PolicyUtil
uses edge.security.fileupload.exception.IllegalContentTypeException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey
uses edge.security.fileupload.IFileUploadPlugin

/**
 * Handler of document uploads.
 * It is needed to work around "security through obscurity" in the authz service and its inability
 * to cope with many different access modes for the same URL. It is also a workaround for a third-party component
 * which is also inflexible and could not accommodate different transports.
 */
class PolicyDocumentUploadHandler extends AbstractRpcHandler {
  private var _policyAuthCheck : IPolicyAccessPlugin
  private var _documentPlugin : IPolicyDocumentsPlugin
  private var _documentSessionPlugin : IDocumentSessionPlugin
  private var _fileUploadPlugin : IFileUploadPlugin

  @InjectableNode
  @Param("policyAuthCheck", "Policy access policy")
  @Param("documentPlugin", "Plugin used to access documents associated with the policy")
  @Param("documentSessionPlugin", "Document session management plugin")
  construct(policyAuthCheck : IPolicyAccessPlugin, documentPlugin : IPolicyDocumentsPlugin,
  documentSessionPlugin : IDocumentSessionPlugin, fileUploadPlugin : IFileUploadPlugin) {
    this._policyAuthCheck = policyAuthCheck
    this._documentPlugin = documentPlugin
    this._documentSessionPlugin = documentSessionPlugin
    this._fileUploadPlugin = fileUploadPlugin
  }


  @JsonRpcMethod
  @ApidocMethodDescription("Uploads a document. " +
      "NOTE: Given a list of file items, it expects that the element at [0] is the document metadata and at [1] is the item to be uploaded.")
  @ApidocAvailableSince("5.0")
  function upload(documentDto:PolicyDocumentUploadDTO, documentFile: FileItem) : PolicyDocumentDTO {
    try {
      try {
        _documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
      } catch (ex : JsonRpcSecurityException) {
        throw new AuthorizationException(){:Message = "Unauthorized portal access"}
      }

      if(documentDto.Name!=documentFile.Name){
        throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
      }

      if(documentDto.MimeType!=documentFile.ContentType) {
        throw new IllegalContentTypeException(DisplayKey.get("Document.upload.contentType.error", documentFile.ContentType))
      }

      if (!_fileUploadPlugin.canUploadContentType(documentFile)) {
        throw new IllegalContentTypeException("Cannot upload files of content type: " + documentFile.ContentType)
      }

      var policyPeriod = PolicyUtil.getLatestPolicyPeriodByPolicyNumber(documentDto.PolicyNumber)

      if (policyPeriod == null) {
        throw new BadIdentifierException("Bad policy number " + documentDto.PolicyNumber)
      }

      if (!_policyAuthCheck.hasAccess(policyPeriod)) {
        throw new NoAuthorityException()
      }


      final var res = Bundle.resolveInTransaction<Document>(\ bundle -> {
        policyPeriod = bundle.add(policyPeriod)
        final var doc = _documentPlugin.createDocumentMetadata(policyPeriod, documentDto)
        Plugins.get(IDocumentContentSource).addDocument(documentFile.InputStream, doc)
        return doc
      })

      /* Should do this outside the transactions as public ID is not set inside the bundle. */
      return _documentPlugin.getDocumentDetails(res)
    } finally {
      documentFile.InputStream.close()
    }
  }
}
The following codeblock contains the updated file content. Highlighted text indicates updates:
package edge.capabilities.policy.document

uses edge.jsonrpc.AbstractRpcHandler
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses org.apache.commons.fileupload2.core.FileItem
uses edge.di.annotations.InjectableNode
uses gw.api.webservice.exception.BadIdentifierException
uses edge.capabilities.policy.auth.IPolicyAccessPlugin
uses edge.security.authorization.exception.NoAuthorityException
uses edge.capabilities.policy.document.dto.PolicyDocumentDTO
uses gw.plugin.Plugins
uses gw.plugin.document.IDocumentContentSource
uses edge.capabilities.policy.document.dto.PolicyDocumentUploadDTO
uses edge.PlatformSupport.Bundle
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.security.authorization.exception.AuthorizationException
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.policy.util.PolicyUtil
uses edge.security.fileupload.exception.IllegalContentTypeException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey
uses org.apache.tika.Tika
uses edge.security.fileupload.FileUploadUtil
uses edge.security.fileupload.DefaultFileUploadPlugin

/**
 * Handler of document uploads.
 * It is needed to work around "security through obscurity" in the authz service and its inability
 * to cope with many different access modes for the same URL. It is also a workaround for a third-party component
 * which is also inflexible and could not accommodate different transports.
 */
class PolicyDocumentUploadHandler extends AbstractRpcHandler {
  private var _policyAuthCheck : IPolicyAccessPlugin
  private var _documentPlugin : IPolicyDocumentsPlugin
  private var _documentSessionPlugin : IDocumentSessionPlugin
  private static final var TIKA = new Tika()
  static final var MIME_PNG = "image/png"
  static final var MIME_JPEG = "image/jpeg"
  static final var MIME_PDF = "application/pdf"

  @InjectableNode
  @Param("policyAuthCheck", "Policy access policy")
  @Param("documentPlugin", "Plugin used to access documents associated with the policy")
  @Param("documentSessionPlugin", "Document session management plugin")
  construct(policyAuthCheck : IPolicyAccessPlugin, documentPlugin : IPolicyDocumentsPlugin,
documentSessionPlugin : IDocumentSessionPlugin) {
    this._policyAuthCheck = policyAuthCheck
    this._documentPlugin = documentPlugin
    this._documentSessionPlugin = documentSessionPlugin
  }

  @JsonRpcMethod
  @ApidocMethodDescription("Uploads a document. " +
      "NOTE: Given a list of file items, it expects that the element at [0] is the document metadata and at [1] is the item to be uploaded.")
  @ApidocAvailableSince("5.0")
  function upload(documentDto:PolicyDocumentUploadDTO, documentFile: FileItem) : PolicyDocumentDTO {
    try {
      try {
        _documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
      } catch (ex : JsonRpcSecurityException) {
        throw new AuthorizationException(){:Message = "Unauthorized portal access"}
      }

      if(documentDto.Name != documentFile.Name){
        throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
      }

         FileUploadUtil.assertSafeFileName(documentDto.Name)
         FileUploadUtil.assertSafeFileName(documentFile.Name)

      // Enforce max upload size
      FileUploadUtil.enforceMaxUploadSize(documentFile)

      // Read file bytes with limit
      var bytes = FileUploadUtil.readAllBytesAndCloseWithLimit(documentFile, FileUploadUtil.getMaxUploadBytes())

      // Detect and normalize MIME type
      var detectedType = FileUploadUtil.normalizeMimeType(TIKA.detect(bytes), documentFile.Name)

      // Enforce PDF name/content consistency
      FileUploadUtil.enforcePdfNameConsistency(documentFile.Name, detectedType, MIME_PDF)

      // Cross-check filename extension against bytes-detected MIME
      FileUploadUtil.assertExtensionMatchesType(documentFile.Name, detectedType)      

      // Centralized allowed content type check
      if (!new DefaultFileUploadPlugin().canUploadContentType(detectedType)) {
        throw new IllegalContentTypeException("Cannot upload files of detected content type: " + detectedType)
      }

      // Validate PDF/image content
      FileUploadUtil.validateContent(detectedType, bytes, MIME_PDF, MIME_PNG, MIME_JPEG)

      // Persist the validated MIME on the DTO so stored metadata matches
      documentDto.MimeType = detectedType

      var policyPeriod = PolicyUtil.getLatestPolicyPeriodByPolicyNumber(documentDto.PolicyNumber)
      if (policyPeriod == null) {
        throw new BadIdentifierException("Bad policy number " + documentDto.PolicyNumber)
      }

      if (!_policyAuthCheck.hasAccess(policyPeriod)) {
        throw new NoAuthorityException()
      }

      final var res = Bundle.resolveInTransaction<Document>(\ bundle -> {
        policyPeriod = bundle.add(policyPeriod)
        final var doc = _documentPlugin.createDocumentMetadata(policyPeriod, documentDto)
        Plugins.get(IDocumentContentSource).addDocument(new java.io.ByteArrayInputStream(bytes), doc)
        return doc
      })

      /* Should do this outside the transactions as public ID is not set inside the bundle. */
      return _documentPlugin.getDocumentDetails(res)
    } finally {
      documentFile.InputStream.close()
    }
  }
}

Changes to the service request file upload handler

Make these changes in gsrc/edge/capabilities/granite_on_cloud-servicerequest/document/ServiceRequestFileUploadHandler.gs:

The following codeblock contains the original file content. Highlighted text indicates content removed or changed in the updated file:
package edge.capabilities.servicerequest.document

uses edge.capabilities.servicerequest.document.dto.ServiceRequestDocumentDTO
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses edge.security.fileupload.IFileUploadPlugin
uses edge.security.fileupload.exception.IllegalContentTypeException
uses org.apache.commons.fileupload2.core.FileItem
uses edge.jsonrpc.annotation.JsonRpcMethod
uses gw.webservice.cc.cc1000.vendormanagement.DocumentContent
uses gw.api.webservice.exception.BadIdentifierException
uses edge.capabilities.servicerequest.local.IServiceRequestRetrievalPlugin
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.security.authorization.Authorizer
uses edge.di.annotations.InjectableNode
uses edge.security.authorization.exception.AuthorizationException
uses edge.PlatformSupport.Bundle
uses gw.plugin.Plugins
uses gw.plugin.document.IDocumentContentSource
uses edge.jsonrpc.AbstractRpcHandler
uses edge.security.authorization.exception.NoAuthorityException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey

class ServiceRequestFileUploadHandler extends AbstractRpcHandler {

  private var _documentPlugin : IServiceRequestDocumentPlugin
  private var _retrievalPlugin : IServiceRequestRetrievalPlugin
  private var _fileUploadPlugin: IFileUploadPlugin
  private var _documentSessionPlugin : IDocumentSessionPlugin
  private var _authz : Authorizer<Document>

  @InjectableNode
  @Param("documentPlugin", "Plugin used to deal with claim documents")
  @Param("documentSessionPlugin", "Plugin used to deal with document sessions")
  @Param("retrievalPlugin", "Plugin used to access service requests")
  @Param("fileUploadPugin", "Plugin used to validate file validity")
  public construct(documentPlugin : IServiceRequestDocumentPlugin,
  documentSessionPlugin : IDocumentSessionPlugin,
  docAuthz:Authorizer<Document>,
  retrievalPlugin : IServiceRequestRetrievalPlugin,
  fileUploadPlugin: IFileUploadPlugin) {
    this._documentPlugin = documentPlugin
    this._documentSessionPlugin = documentSessionPlugin
    this._retrievalPlugin = retrievalPlugin
    this._authz = docAuthz
    this._fileUploadPlugin = fileUploadPlugin
  }

  /**
   * Uploads a document
   *
   * Given a list of file items, it expects that the element
   * <ul>
   * <li> at [0] is the document metadata </li>
   * <li> at [1] is the item to be uploaded </li>
   * </ul>
   *
   * </br>
   * Throws -
   * <ul>
   * <li>IllegalArgumentException If id number associated with the document is null or empty</li>
   * <li>EntityNotFoundException If no service request statement is found</li>
   * <li>AuthorizationException If the portal user has no access to the service request statement</li>
   * </ul>
   *
   * @param list of file items
   * @return document metadata response
   */
  @JsonRpcMethod
  function upload(documentDto:ServiceRequestDocumentDTO, documentFile: FileItem) : ServiceRequestDocumentDTO {
    try {
      try {
        // validate document upload token
        _documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
      } catch (ex : JsonRpcSecurityException) {
        throw new AuthorizationException(){:Message = "Unauthorized portal access"}
      }

      if(documentDto.Name!=documentFile.Name){
        throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
      }

      if(documentDto.MimeType!=documentFile.ContentType) {
        throw new IllegalContentTypeException(DisplayKey.get("Document.upload.contentType.error", documentFile.ContentType))
      }

      if (!_fileUploadPlugin.canUploadContentType(documentFile)) {
        throw new IllegalContentTypeException("Cannot upload files of content type: " + documentFile.ContentType)
      }
      var serviceRequest = _retrievalPlugin.getServiceRequestFromPublicId(documentDto.ServiceRequestId)

      if (serviceRequest == null) {
        throw new BadIdentifierException("Bad service request id " + documentDto.ServiceRequestId)
      }

      final var res = Bundle.resolveInTransaction<Document>(\ bundle -> {
        serviceRequest = bundle.add(serviceRequest)
        var docContent = new DocumentContent(){
            :Content =  documentFile.get(),
            :MimeType = documentDto.MimeType
        };

        final var doc = _documentPlugin.createDocumentMetadata(bundle, serviceRequest, documentDto, docContent)
        if ( !_authz.canAccess(doc) ) {
          throw new NoAuthorityException()
        }
        Plugins.get(IDocumentContentSource).addDocument(documentFile.InputStream, doc)
        return doc
      })

      return  _documentPlugin.toDTO(res)
    } finally {
      documentFile.InputStream.close()
    }
  }
}
The following codeblock contains the updated file content. Highlighted text indicates updates:
package edge.capabilities.servicerequest.document

uses edge.capabilities.servicerequest.document.dto.ServiceRequestDocumentDTO
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses edge.security.fileupload.IFileUploadPlugin
uses edge.security.fileupload.exception.IllegalContentTypeException
uses org.apache.commons.fileupload2.core.FileItem
uses edge.jsonrpc.annotation.JsonRpcMethod
uses gw.webservice.cc.cc1000.vendormanagement.DocumentContent
uses gw.api.webservice.exception.BadIdentifierException
uses edge.capabilities.servicerequest.local.IServiceRequestRetrievalPlugin
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.security.authorization.Authorizer
uses edge.di.annotations.InjectableNode
uses edge.security.authorization.exception.AuthorizationException
uses edge.PlatformSupport.Bundle
uses gw.plugin.Plugins
uses gw.plugin.document.IDocumentContentSource
uses edge.jsonrpc.AbstractRpcHandler
uses edge.security.authorization.exception.NoAuthorityException
uses edge.security.fileupload.FileUploadUtil
uses org.apache.tika.Tika

class ServiceRequestFileUploadHandler extends AbstractRpcHandler {

  private var _documentPlugin : IServiceRequestDocumentPlugin
  private var _retrievalPlugin : IServiceRequestRetrievalPlugin
  private var _fileUploadPlugin: IFileUploadPlugin
  private var _documentSessionPlugin : IDocumentSessionPlugin
  private var _authz : Authorizer<Document>
  private static final var TIKA = new Tika()
  static final var MIME_PNG = "image/png"
  static final var MIME_JPEG = "image/jpeg"
  static final var MIME_PDF = "application/pdf"

  @InjectableNode
  @Param("documentPlugin", "Plugin used to deal with claim documents")
  @Param("documentSessionPlugin", "Plugin used to deal with document sessions")
  @Param("retrievalPlugin", "Plugin used to access service requests")
  @Param("fileUploadPugin", "Plugin used to validate file validity")
  public construct(documentPlugin : IServiceRequestDocumentPlugin,
  documentSessionPlugin : IDocumentSessionPlugin,
  docAuthz:Authorizer<Document>,
  retrievalPlugin : IServiceRequestRetrievalPlugin,
  fileUploadPlugin: IFileUploadPlugin) {
    this._documentPlugin = documentPlugin
    this._documentSessionPlugin = documentSessionPlugin
    this._retrievalPlugin = retrievalPlugin
    this._authz = docAuthz
    this._fileUploadPlugin = fileUploadPlugin
  }

  /**
   * Uploads a document
   *
   * Given a list of file items, it expects that the element
   * <ul>
   * <li> at [0] is the document metadata </li>
   * <li> at [1] is the item to be uploaded </li>
   * </ul>
   *
   * </br>
   * Throws -
   * <ul>
   * <li>IllegalArgumentException If id number associated with the document is null or empty</li>
   * <li>EntityNotFoundException If no service request statement is found</li>
   * <li>AuthorizationException If the portal user has no access to the service request statement</li>
   * </ul>
   *
   * @param list of file items
   * @return document metadata response
   */
  @JsonRpcMethod
  function upload(documentDto:ServiceRequestDocumentDTO, documentFile: FileItem) : ServiceRequestDocumentDTO {
    try {
      try {
        // validate document upload token
        _documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
      } catch (ex : JsonRpcSecurityException) {
        throw new AuthorizationException(){:Message = "Unauthorized portal access"}
      }

      // Reject names with control / bidi / non-printable chars
      FileUploadUtil.assertSafeFileName(documentDto.Name)
      FileUploadUtil.assertSafeFileName(documentFile.Name)

      FileUploadUtil.enforceMaxUploadSize(documentFile)
      var bytes = FileUploadUtil.readAllBytesAndCloseWithLimit(documentFile, FileUploadUtil.getMaxUploadBytes())
      var detectedType = FileUploadUtil.normalizeMimeType(TIKA.detect(bytes), documentFile.Name)
      FileUploadUtil.enforcePdfNameConsistency(documentFile.Name, detectedType, MIME_PDF)
      FileUploadUtil.assertExtensionMatchesType(documentFile.Name, detectedType)

      if (!_fileUploadPlugin.canUploadContentType(detectedType)) {
        throw new IllegalContentTypeException("Cannot upload files of content type: " + detectedType)
      }

      FileUploadUtil.validateContent(detectedType, bytes, MIME_PDF, MIME_PNG, MIME_JPEG)

      documentDto.MimeType = detectedType

      var serviceRequest = _retrievalPlugin.getServiceRequestFromPublicId(documentDto.ServiceRequestId)

      if (serviceRequest == null) {
        throw new BadIdentifierException("Bad service request id " + documentDto.ServiceRequestId)
      }

      final var res = Bundle.resolveInTransaction<Document>(\ bundle -> {
        serviceRequest = bundle.add(serviceRequest)
        var docContent = new DocumentContent(){
            :Content =  bytes,
            :MimeType = detectedType
        };

        final var doc = _documentPlugin.createDocumentMetadata(bundle, serviceRequest, documentDto, docContent)
        if ( !_authz.canAccess(doc) ) {
          throw new NoAuthorityException()
        }
        var uploadInputStream = new java.io.ByteArrayInputStream(bytes)
        try {
          Plugins.get(IDocumentContentSource).addDocument(uploadInputStream, doc)
        } finally {
          uploadInputStream.close()
        }
        return doc
      })

      return  _documentPlugin.toDTO(res)
    } finally {
      documentFile.InputStream.close()
    }
  }
}

New file upload utility

A new FileUploadUtil.gs file has been added:

File contents:
package edge.security.fileupload

uses java.io.ByteArrayInputStream
uses javax.imageio.ImageIO
uses java.io.IOException
uses javax.imageio.IIOException
uses java.nio.charset.StandardCharsets
uses java.net.URLEncoder
uses java.util.regex.Pattern
uses edge.security.fileupload.exception.IllegalContentTypeException
uses gw.api.util.Logger
uses org.apache.commons.fileupload2.core.FileItem
uses java.io.ByteArrayOutputStream
uses org.apache.tika.Tika

public class FileUploadUtil {
  private static final var MAX_UPLOAD_BYTES = 25 * 1024 * 1024 // 25 MB
  private static final var LOG = Logger.forCategory(FileUploadUtil.Type.QName)
  private static final var TIKA = new Tika()

  // Unicode control / bidi / zero-width / non-character ranges that break
  // Content-Disposition serialization and enable RTL-override filename spoofing.
  // Disallowed at upload; stripped at download for any pre-existing stored names.
  // U+061C       Arabic Letter Mark (ALM, bidi formatter)
  // U+200B-200F  zero-width and bidi marks (ZWSP, ZWNJ, ZWJ, LRM, RLM)
  // U+202A-202E  bidi embedding/override (incl. RLO U+202E)
  // U+2066-2069  bidi isolates (LRI, RLI, FSI, PDI -- Trojan Source vectors)
  // U+2028-2029  line/paragraph separators
  // U+FEFF       BOM / zero-width no-break space
  // U+FFFD       replacement character
  // Plus all C0 (0x00-0x1F) and C1 (0x7F-0x9F) control chars.
  private static final var DISALLOWED_NAME_CHAR : String =
      "[\\u0000-\\u001F\\u007F-\\u009F\\u061C\\u200B-\\u200F\\u202A-\\u202E\\u2066-\\u2069\\u2028\\u2029\\uFEFF\\uFFFD]"
  private static final var DISALLOWED_NAME_PATTERN : Pattern = Pattern.compile(DISALLOWED_NAME_CHAR)

  // XENG-2487: characters that enable Content-Disposition parameter injection
  // (`;`), quoted-string breakouts (`"`, `'`), or HTML metachars (`<`, `>`).
  // The download-path header builder already escapes these; reject at upload
  // as defense in depth.
  private static final var HEADER_UNSAFE_CHAR : String = "[\";'<>]"
  private static final var HEADER_UNSAFE_PATTERN : Pattern = Pattern.compile(HEADER_UNSAFE_CHAR)

  private static final var DEFAULT_DOWNLOAD_FILENAME = "document"

  public static function getMaxUploadBytes() : int {
    return MAX_UPLOAD_BYTES
  }

  public static function enforceMaxUploadSize(documentFile : FileItem) {
    var size : long
    try {
      size = documentFile.Size
    } catch (e : Exception) {
      // Fail closed: a missing/unreadable size indicates a malformed upload that
      // we should reject rather than wave through with an unbounded read.
      LOG.warn("Rejected upload: could not determine file size: " + e)
      throw new IllegalContentTypeException("Uploaded file size could not be determined.")
    }
    if (size > MAX_UPLOAD_BYTES) {
      throw new IllegalContentTypeException("Uploaded file exceeds maximum allowed size.")
    }
  }

  public static function readAllBytesAndCloseWithLimit(documentFile : FileItem, maxBytes : int) : byte[] {
    var bos = new ByteArrayOutputStream()
    var is = documentFile.InputStream
    var buffer = new byte[8192]
    var total = 0
    try {
      while (true) {
        var read = is.read(buffer)
        if (read < 0) {
          break
        }
        total += read
        if (total > maxBytes) {
          throw new IllegalContentTypeException("Uploaded file exceeds maximum allowed size.")
        }
        bos.write(buffer, 0, read)
      }
      var bytes = bos.toByteArray()
      if (bytes.length == 0) {
        throw new IllegalContentTypeException("Uploaded file is empty.")
      }
      return bytes
    } finally {
      try {
        is.close()
      } finally {
        bos.close()
      }
    }
  }

  public static function normalizeMimeType(mime : String, fileName : String) : String {
    var m = mime?.trim()?.toLowerCase()
    // Both application/zip and application/x-tika-ooxml are opaque OOXML containers
    // at this detection depth. Use the file extension to identify the specific type.
    if (m == "application/zip" or m == "application/x-tika-ooxml") {
      var mapped = mapZipByExtension(fileName)
      if (mapped != null) {
        return mapped
      }
      // No recognized extension — fall through to standard normalization.
      // canUploadContentType will reject the unknown type.
    }
    // application/x-tika-msoffice is Tika's generic supertype for legacy OLE2 binary
    // Office formats (.doc, .xls, .ppt, .msg). Palisades upgraded Tika returns this
    // instead of the specific canonical MIME types that pre-date OOXML. Use the file
    // extension to map back to the canonical type so the allowlist check can match.
    if (m == "application/x-tika-msoffice") {
      var mapped = mapOleByExtension(fileName)
      if (mapped != null) {
        return mapped
      }
      // No recognized extension — fall through; canUploadContentType will reject it.
    }
    // Tika sniffs CSV bytes as text/plain (the content is plain text), but the
    // file extension implies text/csv. Normalize so assertExtensionMatchesType
    // and the allowlist check both pass.
    if (m == "text/plain") {
      var n = fileName?.trim()?.toLowerCase()
      if (n != null and n.endsWith(".csv")) {
        return "text/csv"
      }
    }
    // video/quicktime is Tika's catch-all for the ISO Base Media File Format (ISOBMFF)
    // container family. MP4 files whose ftyp major brand is not one of the specific
    // brands Tika has magic bytes for (mp41, mp42) are detected as video/quicktime even
    // though they carry a .mp4 extension. Use the file extension to resolve to the
    // correct specific type, mirroring the zip→OOXML and msoffice→OLE patterns above.
    if (m == "video/quicktime") {
      var mapped = mapQuicktimeByExtension(fileName)
      if (mapped != null) {
        return mapped
      }
      // .mov / .qt / unrecognised extension — stays video/quicktime.
    }
    return normalizeMimeType(mime)
  }

  public static function normalizeMimeType(mime : String) : String {
    if (mime == null) {
      return null
    }
    var m = mime.trim().toLowerCase()
    if (m == "image/jpg") {
      return "image/jpeg"
    }
    if (m == "application/x-pdf") {
      return "application/pdf"
    }
    // RFC 4337 defines application/mp4 as the generic MP4 container type.
    // Tika may return this from extension-based detection while returning video/mp4
    // from bytes-based detection (or vice versa). Normalize to video/mp4 so the
    // assertExtensionMatchesType comparison and the allowlist check both succeed.
    if (m == "application/mp4") {
      return "video/mp4"
    }
    // Tika 3.x canonical type for WAV is audio/vnd.wave; audio/x-wav and audio/wave
    // are registered aliases. The allowlist entry is audio/wav (the IANA short form).
    // Normalize all three aliases so the allowlist check and extension comparison succeed.
    if (m == "audio/vnd.wave" or m == "audio/x-wav" or m == "audio/wave") {
      return "audio/wav"
    }
    // image/x-bmp and image/x-ms-bmp are legacy aliases for image/bmp (IANA-registered
    // since RFC 7903). Tika 3.x returns the canonical image/bmp but normalize the aliases
    // as defence-in-depth in case a client or older detector path returns one.
    if (m == "image/x-bmp" or m == "image/x-ms-bmp") {
      return "image/bmp"
    }
    // application/x-tika-ooxml and application/x-tika-msoffice are intentionally NOT
    // resolved here — a filename is required. Use normalizeMimeType(mime, fileName).
    return m
  }

  private static function mapZipByExtension(fileName : String) : String {
    if (fileName == null) {
      return null
    }
    var n = fileName.trim().toLowerCase()
    if (n.endsWith(".docx")) {
      return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
    }
    if (n.endsWith(".xlsx")) {
      return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    }
    if (n.endsWith(".pptx")) {
      return "application/vnd.openxmlformats-officedocument.presentationml.presentation"
    }
    return null
  }

  private static function mapOleByExtension(fileName : String) : String {
    if (fileName == null) {
      return null
    }
    var n = fileName.trim().toLowerCase()
    if (n.endsWith(".doc") or n.endsWith(".dot")) {
      return "application/msword"
    }
    if (n.endsWith(".xls") or n.endsWith(".xlt") or n.endsWith(".xlm")) {
      return "application/vnd.ms-excel"
    }
    if (n.endsWith(".ppt") or n.endsWith(".pot") or n.endsWith(".pps")) {
      return "application/vnd.ms-powerpoint"
    }
    if (n.endsWith(".msg")) {
      return "application/vnd.ms-outlook"
    }
    return null
  }

  // Tika detects the QuickTime/ISOBMFF container family as video/quicktime when the
  // file's ftyp major brand does not match one of the more specific magic-byte patterns
  // (e.g. ftypmp41/mp42 for video/mp4). Use the file extension to resolve to the
  // correct allowlisted subtype. .mov and .qt stay as video/quicktime (their own glob
  // entries in Tika); anything unrecognised returns null so the caller falls through.
  private static function mapQuicktimeByExtension(fileName : String) : String {
    if (fileName == null) {
      return null
    }
    var n = fileName.trim().toLowerCase()
    if (n.endsWith(".mp4") or n.endsWith(".mp4v") or n.endsWith(".mpg4")) {
      return "video/mp4"
    }
    // .mov and .qt are the native QuickTime extensions — keep video/quicktime.
    if (n.endsWith(".mov") or n.endsWith(".qt")) {
      return "video/quicktime"
    }
    return null
  }

  public static function isDecodableImage(bytes : byte[]) : boolean {
    try {
      return ImageIO.read(new ByteArrayInputStream(bytes)) != null
    } catch (e : IIOException) {
      LOG.debug("Image decode failed")
      return false
    } catch (e : IOException) {
      LOG.debug("Image decode failed")
      return false
    } catch (e : RuntimeException) {
      LOG.debug("Image decode failed")
      return false
    }
  }

  public static function passesPdfSanityCheck(bytes : byte[]) : boolean {
    if (bytes == null or bytes.length < 10) {
      return false
    }
    var headerLen = Math.min(8, bytes.length)
    var header = new String(bytes, 0, headerLen, StandardCharsets.ISO_8859_1)
    if (!header.startsWith("%PDF-")) {
      return false
    }
    var tailWindow = Math.min(64 * 1024, bytes.length)
    var tail = new String(bytes, bytes.length - tailWindow, tailWindow, StandardCharsets.ISO_8859_1)
    var eofIndex = tail.lastIndexOf("%%EOF")
    if (eofIndex < 0) {
      return false
    }
    var startxrefIndex = tail.lastIndexOf("startxref")
    if (startxrefIndex < 0 or startxrefIndex > eofIndex) {
      return false
    }
    var hasClassicXref = tail.contains("\nxref") or tail.contains("\rxref")
    var hasClassicTrailer = tail.contains("trailer")
    var hasXrefStreamMarker = tail.contains("/XRef")
    return (hasClassicXref and hasClassicTrailer) or hasXrefStreamMarker
  }

  public static function enforcePdfNameConsistency(fileName : String, detectedType : String, mimePdf : String) {
    if (fileName == null) {
      return
    }
    var n = fileName.trim().toLowerCase()
    var looksLikePdf = n.endsWith(".pdf") or n.contains(".pdf.")
    if (looksLikePdf and detectedType != mimePdf) {
      LOG.info("Rejected upload: filename indicates PDF but detected content is not PDF")
      throw new IllegalContentTypeException("File name indicates a PDF, but the uploaded file is not a valid PDF.")
    }
  }

  public static function validateContent(detectedType : String, bytes : byte[], mimePdf : String, mimePng : String, mimeJpeg : String) {
    switch (detectedType) {
      case mimePdf:
        if (!passesPdfSanityCheck(bytes)) {
          LOG.info("Rejected PDF upload: invalid content")
          throw new IllegalContentTypeException("The uploaded PDF appears to be invalid or corrupted. Please re-save and try again.")
        }
        break
      case mimePng:
      case mimeJpeg:
        if (!isDecodableImage(bytes)) {
          LOG.info("Rejected image upload: invalid content")
          throw new IllegalContentTypeException("The uploaded image appears to be invalid or corrupted. Please re-save and try again.")
        }
        break
      default:
        break
    }
  }

  // Reject file names containing Unicode control / bidi / zero-width / non-printable chars.
  // These characters break Content-Disposition serialization (causing browsers to render
  // attachments inline) and enable RTL-override extension spoofing.
  public static function assertSafeFileName(fileName : String) {
    if (fileName == null or fileName.length == 0) {
      throw new IllegalContentTypeException("File name is required.")
    }
    var matcher = DISALLOWED_NAME_PATTERN.matcher(fileName)
    if (matcher.find()) {
      // Report the first offending char as U+XXXX. Never echo the raw character
      // into the log or exception message — control/bidi chars can break log
      // parsers and enable log-injection.
      var codePoint = formatCodePoint(fileName.charAt(matcher.start()) as int)
      LOG.info("Rejected upload: file name contains disallowed character " + codePoint)
      throw new IllegalContentTypeException(
          "File name contains disallowed control or formatting character (" + codePoint + ").")
    }
    // XENG-2487: reject header-injection / quote-breakout / HTML metachars.
    var injMatcher = HEADER_UNSAFE_PATTERN.matcher(fileName)
    if (injMatcher.find()) {
      var ch = fileName.charAt(injMatcher.start())
      LOG.info("Rejected upload: file name contains disallowed character '" + ch + "'")
      throw new IllegalContentTypeException("File name must not contain '" + ch + "'.")
    }
    // Reject path separators so a malicious name cannot influence storage paths.
    if (fileName.contains("/") or fileName.contains("\\")) {
      LOG.info("Rejected upload: file name contains path separator")
      throw new IllegalContentTypeException("File name must not contain path separators.")
    }
  }

  private static function formatCodePoint(code : int) : String {
    var hex = Integer.toHexString(code).toUpperCase()
    var sb = new StringBuilder("U+")
    for (i in 0..|(4 - hex.length)) {
      sb.append("0")
    }
    sb.append(hex)
    return sb.toString()
  }

  // Strip disallowed characters. Used as defense-in-depth on the download path so any
  // names stored before the upload-side fix do not break the Content-Disposition header.
  public static function sanitizeFileName(fileName : String) : String {
    if (fileName == null) {
      return DEFAULT_DOWNLOAD_FILENAME
    }
    var stripped = DISALLOWED_NAME_PATTERN.matcher(fileName).replaceAll("")
        .replaceAll("[\\\\/]", "_")
        .trim()
    return stripped.length == 0 ? DEFAULT_DOWNLOAD_FILENAME : stripped
  }

  // Build an RFC 6266 Content-Disposition header value: an ASCII-only quoted fallback
  // plus a UTF-8 percent-encoded `filename*` for compliant clients. Always returns
  // `attachment; ...` so the browser never renders the document inline.
  public static function buildContentDisposition(fileName : String) : String {
    var safeName = sanitizeFileName(fileName)
    var asciiFallback = toAsciiFallback(safeName)
    var utf8Encoded = percentEncodeUtf8(safeName)
    return "attachment; filename=\"" + asciiFallback + "\"; filename*=UTF-8''" + utf8Encoded
  }

  // Replace anything outside printable ASCII (0x20-0x7E) with "_" and backslash-escape
  // any embedded quote or backslash so the value is safe inside the quoted-string per RFC 6266.
  private static function toAsciiFallback(name : String) : String {
    var sb = new StringBuilder()
    for (i in 0..|name.length) {
      var code = (name.charAt(i) as int)
      if (code < 0x20 or code > 0x7E) {
        sb.append("_")
      } else if (code == 0x22 or code == 0x5C) { // " or \
        sb.append("\\").append(name.charAt(i))
      } else {
        sb.append(name.charAt(i))
      }
    }
    var result = sb.toString()
    return result.length == 0 ? DEFAULT_DOWNLOAD_FILENAME : result
  }

  private static function percentEncodeUtf8(name : String) : String {
    try {
      // URLEncoder follows application/x-www-form-urlencoded rules, which differ
      // from RFC 5987 attr-char in two places: spaces become "+" (we convert to
      // %20) and "*" is left literal (we percent-encode to %2A so the filename*
      // value stays RFC-compliant for filenames like "budget*final.pdf").
      return URLEncoder.encode(name, StandardCharsets.UTF_8.name())
          .replace("+", "%20")
          .replace("*", "%2A")
    } catch (e : java.io.UnsupportedEncodingException) {
      LOG.debug("UTF-8 encoding unavailable when building Content-Disposition: " + e)
      return DEFAULT_DOWNLOAD_FILENAME
    }
  }

  // Cross-check the filename's implied content type (from extension via Tika)
  // against the bytes-detected type. Catches the `evil.exe`-with-image-bytes
  // pattern where bytes pass the allow-list but the filename extension would
  // not. Skips the check when the extension is unknown to Tika so files like
  // README (no extension) still go through.
  public static function assertExtensionMatchesType(fileName : String, detectedType : String) {
    if (fileName == null) {
      return
    }
    var extType : String
    try {
      extType = TIKA.detect(fileName)
    } catch (e : Exception) {
      LOG.debug("Could not infer type from filename '" + fileName + "': " + e)
      return
    }
    if (extType == null or extType == "application/octet-stream") {
      return
    }
    var normalizedExt = normalizeMimeType(extType, fileName)
    if (normalizedExt != detectedType) {
      LOG.info("Rejected upload: filename extension implies " + normalizedExt
          + " but content sniffs as " + detectedType)
      throw new IllegalContentTypeException(
          "File extension does not match detected content type.")
    }
  }
}

Changes to the claim document upload handler

The following sections show the original and updated code. Bold lines in each code block indicate where the changes were made for this fix. Make these changes in ClaimDocumentUploadHandler.gs:

The following codeblock contains the original file content. Highlighted text indicates content removed or changed in the updated file:
package edge.capabilities.claim.document


uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses org.apache.commons.fileupload2.core.FileItem

uses edge.jsonrpc.AbstractRpcHandler
uses edge.di.annotations.InjectableNode
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.claim.local.IClaimRetrievalPlugin
uses gw.api.webservice.exception.BadIdentifierException
uses edge.PlatformSupport.Bundle
uses gw.plugin.Plugins
uses gw.plugin.document.IDocumentContentSource
uses edge.security.authorization.exception.AuthorizationException
uses edge.capabilities.claim.document.dto.ClaimDocumentUploadDTO
uses edge.capabilities.claim.document.dto.ClaimDocumentDTO
uses edge.capabilities.claim.document.IClaimDocumentPlugin
uses edge.security.fileupload.exception.IllegalContentTypeException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey
uses edge.security.fileupload.IFileUploadPlugin

/**
 * Handler of document uploads.
 * It is needed to work around "security through obscurity" in the authz service and its inability
 * to cope with many different access modes for the same URL. It is also a workaround for a third-party component
 * which is also inflexible and could not accommodate different transports.
 */
class ClaimDocumentUploadHandler extends AbstractRpcHandler {

  private var _documentPlugin : IClaimDocumentPlugin
  private var _documentSessionPlugin : IDocumentSessionPlugin
  private var _claimRetrievalPlugin : IClaimRetrievalPlugin
  private var _fileUploadPlugin : IFileUploadPlugin

  @InjectableNode
  @Param("documentPlugin", "Plugin used to deal with claim documents")
  @Param("documentSessionPlugin", "Plugin used to deal with document sessions")
  @Param("claimRetrievalPlugin", "Plugin used to access claim data")
  @Param("fileUploadPlugin", "Plugin used to verify file upload validity")
  construct(documentPlugin : IClaimDocumentPlugin,
  documentSessionPlugin : IDocumentSessionPlugin,
  claimRetrievalPlugin : IClaimRetrievalPlugin,
  fileUploadPlugin : IFileUploadPlugin) {
    this._documentPlugin = documentPlugin
    this._documentSessionPlugin = documentSessionPlugin
    this._claimRetrievalPlugin = claimRetrievalPlugin
    this._fileUploadPlugin = fileUploadPlugin
  }

  /**
   * Used to upload claim documents from the frontend.
   *
   * <dl>
   *  <dt>Calls:</dt>
   *  <dd><code>IDocumentSessionPlugin#isSessionValid(String)</code> - to check if the current document session token is valid in this context.</dd>
   *  <dd><code>IFileUploadPlugin#canUploadContentType(FileItem)</code> - to see if the filetype for the document is allowed by the DMS.</dd>
   *  <dd><code>IClaimRetrievalPlugin#getClaimByNumber(String)</code> - to retrieve the claim.</dd>
   *  <dd><code>IClaimDocumentPlugin#createDocumentMetadata(Bundle, Claim, ClaimDocumentUploadDTO)</code> - to convert DTO into metadata and attach it to the document.</dd>
   *  <dt>Throws:</dt>
   *  <dd><code>AuthorizationException</code> - If session is invalid</dd>
   *  <dd><code>IllegalContentTypeException</code> - If the filetype is not permitted by the DMS</dd>
   *  <dd><code>BadIdentifierException</code> - If the claim number does not exist</dd>
   * </dl>
   *
   * @param documentDto a DTO describing the metadata for the document
   * @param documentFile the document itself
   * @returns A claimDocumentDTO for display in the frontend
   * */
  @JsonRpcMethod
  @ApidocMethodDescription("Used to upload claim documents from the frontend.")
  @ApidocAvailableSince("5.0")
  function upload(documentDto:ClaimDocumentUploadDTO, documentFile: FileItem) : ClaimDocumentDTO {
    try {
      try {
        // validate document upload token
        _documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
      } catch ( ex : JsonRpcSecurityException) {
        throw new AuthorizationException(){:Message = "Unauthorized portal access"}
      }

      if(documentDto.Name!=documentFile.Name){
        throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
      }

      if(documentDto.MimeType!=documentFile.ContentType) {
        throw new IllegalContentTypeException(DisplayKey.get("Document.upload.contentType.error", documentFile.ContentType))
      }

      if (!_fileUploadPlugin.canUploadContentType(documentFile)) {
        throw new IllegalContentTypeException("Cannot upload files of content type: " + documentFile.ContentType)
      }

      var claim = _claimRetrievalPlugin.getClaimByNumber(documentDto.ClaimNumber)

      if (claim == null) {
        throw new BadIdentifierException("Bad claim number " + documentDto.ClaimNumber)
      }

      final var res = Bundle.resolveInTransaction<Document>(\ bundle -> {
        claim = bundle.add(claim)
        final var doc = _documentPlugin.createDocumentMetadata(bundle, claim, documentDto)
        Plugins.get(IDocumentContentSource).addDocument(documentFile.InputStream, doc)
        return doc
      })

      /* Should do this outside the transactions as public ID is not set inside the bundle. */
      return _documentPlugin.getDocumentDetails(res)
    } finally {
      documentFile.InputStream.close()
    }
  }
}
The following codeblock contains the updated file content. Highlighted text indicates updates:
package edge.capabilities.claim.document

uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses org.apache.commons.fileupload2.core.FileItem
uses org.apache.tika.Tika
uses gw.api.util.Logger
uses edge.jsonrpc.AbstractRpcHandler
uses edge.di.annotations.InjectableNode
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.claim.local.IClaimRetrievalPlugin
uses gw.api.webservice.exception.BadIdentifierException
uses edge.PlatformSupport.Bundle
uses gw.plugin.Plugins
uses gw.plugin.document.IDocumentContentSource
uses edge.security.authorization.exception.AuthorizationException
uses edge.capabilities.claim.document.dto.ClaimDocumentUploadDTO
uses edge.capabilities.claim.document.dto.ClaimDocumentDTO
uses edge.security.fileupload.exception.IllegalContentTypeException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey
uses edge.security.fileupload.IFileUploadPlugin
uses edge.security.fileupload.FileUploadUtil

/**
 * Handler of document uploads.
 * It is needed to work around "security through obscurity" in the authz service and its inability
 * to cope with many different access modes for the same URL. It is also a workaround for a third-party component
 * which is also inflexible and could not accommodate different transports.
 */
class ClaimDocumentUploadHandler extends AbstractRpcHandler {

  private static final var LOG = Logger.forCategory(ClaimDocumentUploadHandler.Type.QName)
  private static final var TIKA = new Tika()
  static final var MIME_PNG = "image/png"
  static final var MIME_JPEG = "image/jpeg"
  static final var MIME_PDF = "application/pdf"

  private var _documentPlugin : IClaimDocumentPlugin
  private var _documentSessionPlugin : IDocumentSessionPlugin
  private var _claimRetrievalPlugin : IClaimRetrievalPlugin
  private var _fileUploadPlugin : IFileUploadPlugin

  @InjectableNode
  @Param("documentPlugin", "Plugin used to deal with claim documents")
  @Param("documentSessionPlugin", "Plugin used to deal with document sessions")
  @Param("claimRetrievalPlugin", "Plugin used to access claim data")
  @Param("fileUploadPlugin", "Plugin used to verify file upload validity")
  construct(documentPlugin : IClaimDocumentPlugin,
  documentSessionPlugin : IDocumentSessionPlugin,
  claimRetrievalPlugin : IClaimRetrievalPlugin,
  fileUploadPlugin : IFileUploadPlugin) {
    this._documentPlugin = documentPlugin
    this._documentSessionPlugin = documentSessionPlugin
    this._claimRetrievalPlugin = claimRetrievalPlugin
    this._fileUploadPlugin = fileUploadPlugin
  }

  /**
   * Used to upload claim documents from the frontend.
   *
   * <dl>
   *  <dt>Calls:</dt>
   *  <dd><code>IDocumentSessionPlugin#isSessionValid(String)</code> - to check if the current document session token is valid in this context.</dd>
   *  <dd><code>IFileUploadPlugin#canUploadContentType(FileItem)</code> - to see if the filetype for the document is allowed by the DMS.</dd>
   *  <dd><code>IClaimRetrievalPlugin#getClaimByNumber(String)</code> - to retrieve the claim.</dd>
   *  <dd><code>IClaimDocumentPlugin#createDocumentMetadata(Bundle, Claim, ClaimDocumentUploadDTO)</code> - to convert DTO into metadata and attach it to the document.</dd>
   *  <dt>Throws:</dt>
   *  <dd><code>AuthorizationException</code> - If session is invalid</dd>
   *  <dd><code>IllegalContentTypeException</code> - If the filetype is not permitted by the DMS</dd>
   *  <dd><code>BadIdentifierException</code> - If the claim number does not exist</dd>
   * </dl>
   *
   * @param documentDto a DTO describing the metadata for the document
   * @param documentFile the document itself
   * @returns A claimDocumentDTO for display in the frontend
   * */
  @JsonRpcMethod
  @ApidocMethodDescription("Used to upload claim documents from the frontend.")
  @ApidocAvailableSince("5.0")
  function upload(documentDto:ClaimDocumentUploadDTO, documentFile: FileItem) : ClaimDocumentDTO {
    try {
      try {
        // validate document upload token
        _documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
      } catch ( ex : JsonRpcSecurityException) {
        throw new AuthorizationException(){:Message = "Unauthorized portal access"}
      }

      if(documentDto.Name!=documentFile.Name){
        throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
      }

      FileUploadUtil.assertSafeFileName(documentDto.Name)
      FileUploadUtil.assertSafeFileName(documentFile.Name)

      FileUploadUtil.enforceMaxUploadSize(documentFile)

      var bytes = FileUploadUtil.readAllBytesAndCloseWithLimit(documentFile, FileUploadUtil.getMaxUploadBytes())

      var detectedType = FileUploadUtil.normalizeMimeType(TIKA.detect(bytes), documentFile.Name)

      FileUploadUtil.enforcePdfNameConsistency(documentFile.Name, detectedType, MIME_PDF)

      FileUploadUtil.assertExtensionMatchesType(documentFile.Name, detectedType)

      if (!_fileUploadPlugin.canUploadContentType(detectedType)) {
       throw new IllegalContentTypeException("Cannot upload files of detected content type: " + detectedType)
      }

      FileUploadUtil.validateContent(detectedType, bytes, MIME_PDF, MIME_PNG, MIME_JPEG)

      documentDto.MimeType = detectedType

      var claim = _claimRetrievalPlugin.getClaimByNumber(documentDto.ClaimNumber)
      if (claim == null) {
        throw new BadIdentifierException("Bad claim number " + documentDto.ClaimNumber)
      }

      final var res = Bundle.resolveInTransaction<Document>(\ bundle -> {
        claim = bundle.add(claim)
        final var doc = _documentPlugin.createDocumentMetadata(bundle, claim, documentDto)
        var uploadInputStream = new java.io.ByteArrayInputStream(bytes)
        try {
          Plugins.get(IDocumentContentSource).addDocument(uploadInputStream, doc)
        } finally {
          uploadInputStream.close()
        }
        return doc
      })

      return _documentPlugin.getDocumentDetails(res)
    } finally {
      documentFile.InputStream.close()
    }
  }
}

Updated package versions

Guidewire has modified the configuration of certain package versions for improved product security. Customers who maintain customized back-end deployments must apply these changes manually.

Jackson databind package update

InsuranceSuite application affected: PolicyCenter, ClaimCenter, BillingCenter

Digital reference application affected: CustomerEngage Account Management, CustomerEngage Account Management for ClaimCenter, CustomerEngage Quote and Buy, ProducerEngage, ProducerEngage for ClaimCenter, ServiceRepEngage, VendorEngage

  • Package: com.fasterxml.jackson.core:jackson-databind
  • Updated version: 2.18.9
  • Files changed: Platform-Java/gosu-jackson-support/pom.xml, pom.xml

Platform-Java/gosu-jackson-support/pom.xml

Change made:
...
<dependencies>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
-            <version>2.18.6</version>
+            <version>2.18.9</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
...

pom.xml

Changes made:
...
</dependency>
+    <dependency>
+           <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>2.18.9</version>
+    </dependency>
</dependencies>
    </dependencyManagement>
...

2025.11.3 release

This section lists all of the changes added in patch 3 of the 2025.11 release.

New Jutro version

InsuranceSuite application affected: N/A

Digital reference application affected: All Digital reference applications

Digital reference applications now use Jutro patch version 10.12.5. This brings enhanced security to the Jutro Design System.

ProducerEngage gateway account API endpoint changes

InsuranceSuite application affected: PolicyCenter

Digital reference application affected: ProducerEngage

Guidewire has modified the configuration of the ProducerEngage /gateway/account API endpoint for improved security. There are steps that you can take to have improved security and need to manually add the changes to your own files.

After making this change, ServiceRepEngage users must ensure that they have the correct producer codes assigned to their account in PolicyCenter in order to create quotes.

The following sections show the original and updated code. Bold lines in each code block indicate where the changes were made for this fix. Make the following changes to /gsrc/edge/capabilities/gateway/account/AccountHandler.gs:

Original file content:

@JsonRpcRunAsInternalGWUser
  @JsonRpcMethod
  @ApidocMethodDescription("Returns or creates and returns the given account.")
  @ApidocAvailableSince("4.0")
  function getOrCreateAccount(anAccountDTO: AccountDTO): AccountDTO {
    if (anAccountDTO.AccountNumber == null) {
      final var newAccount = _accountPlugin.createAccount(anAccountDTO)
      return _accountPlugin.accountBaseDetailsToDTO(newAccount)
    }

    final var account = _accountRetrievalPlugin.getAccountByNumber(anAccountDTO.AccountNumber)

    return _accountPlugin.accountBaseDetailsToDTO(account)
  }

Updated file content:

@JsonRpcRunAsInternalGWUser
  @JsonRpcMethod
  @ApidocMethodDescription("Returns or creates and returns the given account.")
  @ApidocAvailableSince("4.0")
  function getOrCreateAccount(anAccountDTO: AccountDTO): AccountDTO {
    if (anAccountDTO.AccountNumber == null) {
      verifyProducerCodeOwnership(anAccountDTO)
      final var newAccount = _accountPlugin.createAccount(anAccountDTO)
      return _accountPlugin.accountBaseDetailsToDTO(newAccount)
    }

    final var account = _accountRetrievalPlugin.getAccountByNumber(anAccountDTO.AccountNumber)

    return _accountPlugin.accountBaseDetailsToDTO(account)
  }

  private function verifyProducerCodeOwnership(anAccountDTO: AccountDTO) {
    if (anAccountDTO.ProducerCodes == null || !anAccountDTO.ProducerCodes.HasElements) {
      return
    }
    final var ownedCodes = User.util.CurrentUser.UserProducerCodes*.ProducerCode*.Code.toSet()
    final var unauthorized = anAccountDTO.ProducerCodes.firstWhere(\pc -> !ownedCodes.contains(pc.Code))
    if (unauthorized != null) {
      throw new AuthorizationException() {:Message = "User is not authorized to create accounts under producer code: " + unauthorized.Code}
    }
  }

ProducerEngage API search result changes

InsuranceSuite application affected: PolicyCenter

Digital reference application affected: ProducerEngage

Guidewire has modified the configuration of the ProducerEngage API search response for improved security. There are steps that you can take to have improved security and need to manually add the changes to your own files.

You must modify the following files:
  • /gsrc/edge/capabilities/gateway/account/AccountHandler.gs‎
  • /gsrc/edge/capabilities/gateway/account/DefaultAccountPlugin.gs‎
  • /gsrc/edge/capabilities/gateway/account/IAccountPlugin.gs‎
You must also create the following files and update them:
  • /gsrc/edge/capabilities/gateway/account/dto/AccountSearchHolderDTO.gs‎
  • /gsrc/edge/capabilities/gateway/account/dto/AccountSearchResultDTO.gs‎

The following sections show the original and updated code. Bold lines in each code block indicate where the changes were made for this fix.

AccountHandler changes

Make the following changes to /gsrc/edge/capabilities/gateway/account/AccountHandler.gs‎:

Original file content:
package edge.capabilities.gateway.account

uses edge.jsonrpc.AbstractRpcHandler
uses edge.capabilities.gateway.account.dto.AccountJobsSummaryDTO
uses edge.aspects.validation.annotations.Context
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.el.Expr
uses edge.di.annotations.InjectableNode
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.gateway.account.dto.AccountDTO
uses edge.jsonrpc.annotation.JsonRpcRunAsInternalGWUser
uses edge.capabilities.gateway.account.search.dto.AccountSearchCriteriaDTO
uses edge.capabilities.gateway.account.search.IAccountSearchPlugin
uses edge.capabilities.gateway.job.submission.dto.ProductSelectionDTO
uses edge.capabilities.gateway.job.submission.dto.NewSubmissionDTO
uses edge.capabilities.gateway.job.submission.ISubmissionPlugin
uses edge.capabilities.gateway.note.dto.NoteDTO
uses edge.capabilities.gateway.note.INotePlugin
uses edge.capabilities.helpers.GatewayJobUtil
uses edge.capabilities.gateway.document.IDocumentPlugin
uses edge.capabilities.gateway.document.dto.DocumentDTO
uses edge.capabilities.gateway.billing.dto.BillingInvoiceDTO
uses edge.capabilities.gateway.billing.IPolicyPeriodBillingSummaryPlugin
uses edge.capabilities.gateway.billing.dto.PolicyPeriodBillingSummaryDTO
uses java.lang.Exception

uses edge.security.authorization.exception.AuthorizationException
uses edge.security.permission.IPermissionCheckPlugin
uses java.lang.SecurityException
uses edge.capabilities.gateway.claim.IClaimSummaryPlugin
uses edge.capabilities.gateway.claim.dto.ClaimSummaryDTO
uses edge.PlatformSupport.Logger
uses edge.PlatformSupport.Reflection
uses edge.jsonrpc.exception.JsonRpcInternalErrorException
uses edge.exception.EntityNotFoundException
uses edge.capabilities.gateway.account.dto.AccountSummaryDTO
uses edge.capabilities.gateway.account.dto.AccountJobsDTO
uses java.lang.Integer
uses edge.capabilities.gateway.job.dto.JobSummaryDTO
uses edge.capabilities.gateway.job.IJobSummaryPlugin
uses edge.capabilities.gateway.billing.IAccountBillingPlugin
uses edge.capabilities.gateway.billing.dto.AccountBillingDTO
uses edge.capabilities.gateway.account.dto.PolicyTransactionSearchCriteriaDTO
uses edge.capabilities.gateway.account.search.dto.AccountSearchSummaryDTO
uses gw.api.database.Query
uses edge.PlatformSupport.Bundle
uses edge.capabilities.helpers.pagination.dto.QueryOptionsDTO
uses edge.capabilities.helpers.pagination.dto.QueryParameterDTO
uses java.util.ArrayList
@JsonRpcRunAsInternalGWUser
  @JsonRpcMethod
  @ApidocMethodDescription("Returns a list of potentially existing accounts.")
  @ApidocAvailableSince("4.0")
  function getPotentialExistingAccounts(accountSearchCriteriaDTO: AccountSearchCriteriaDTO): AccountDTO[] {
    if(!perm.System.searchaccounts) {
      throw new AuthorizationException(){:Message = "User is not authorized to search accounts"}
    }
    var anAccountSearchCriteria = _accountSearchPlugin.createSearchCriteria(accountSearchCriteriaDTO)

    if (accountSearchCriteriaDTO.ContactType == ContactType.TC_COMPANY) {
      /*If company name is greater than 5 characters then do an exact match*/
      anAccountSearchCriteria.CompanyNameExact = (anAccountSearchCriteria.CompanyName.length < 5)
    } else if (accountSearchCriteriaDTO.ContactType == ContactType.TC_PERSON) {
      anAccountSearchCriteria.LastNameExact = (anAccountSearchCriteria.LastName.length < 5)
      anAccountSearchCriteria.FirstNameExact = (anAccountSearchCriteria.FirstName.length < 5)
    }

    final var potentialAccounts = anAccountSearchCriteria.performSearch()*.Account

    return _accountPlugin.toDTOArray(potentialAccounts)
  }
Updated file content:
package edge.capabilities.gateway.account

uses edge.jsonrpc.AbstractRpcHandler
uses edge.capabilities.gateway.account.dto.AccountJobsSummaryDTO
uses edge.aspects.validation.annotations.Context
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.el.Expr
uses edge.di.annotations.InjectableNode
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.gateway.account.dto.AccountDTO
uses edge.jsonrpc.annotation.JsonRpcRunAsInternalGWUser
uses edge.capabilities.gateway.account.search.dto.AccountSearchCriteriaDTO
uses edge.capabilities.gateway.account.search.IAccountSearchPlugin
uses edge.capabilities.gateway.job.submission.dto.ProductSelectionDTO
uses edge.capabilities.gateway.job.submission.dto.NewSubmissionDTO
uses edge.capabilities.gateway.job.submission.ISubmissionPlugin
uses edge.capabilities.gateway.note.dto.NoteDTO
uses edge.capabilities.gateway.note.INotePlugin
uses edge.capabilities.helpers.GatewayJobUtil
uses edge.capabilities.gateway.document.IDocumentPlugin
uses edge.capabilities.gateway.document.dto.DocumentDTO
uses edge.capabilities.gateway.billing.dto.BillingInvoiceDTO
uses edge.capabilities.gateway.billing.IPolicyPeriodBillingSummaryPlugin
uses edge.capabilities.gateway.billing.dto.PolicyPeriodBillingSummaryDTO
uses java.lang.Exception

uses edge.security.authorization.exception.AuthorizationException
uses edge.security.permission.IPermissionCheckPlugin
uses java.lang.SecurityException
uses edge.capabilities.gateway.claim.IClaimSummaryPlugin
uses edge.capabilities.gateway.claim.dto.ClaimSummaryDTO
uses edge.PlatformSupport.Logger
uses edge.PlatformSupport.Reflection
uses edge.jsonrpc.exception.JsonRpcInternalErrorException
uses edge.exception.EntityNotFoundException
uses edge.capabilities.gateway.account.dto.AccountSummaryDTO
uses edge.capabilities.gateway.account.dto.AccountJobsDTO
uses java.lang.Integer
uses edge.capabilities.gateway.job.dto.JobSummaryDTO
uses edge.capabilities.gateway.job.IJobSummaryPlugin
uses edge.capabilities.gateway.billing.IAccountBillingPlugin
uses edge.capabilities.gateway.billing.dto.AccountBillingDTO
uses edge.capabilities.gateway.account.dto.PolicyTransactionSearchCriteriaDTO
uses edge.capabilities.gateway.account.search.dto.AccountSearchSummaryDTO
uses edge.capabilities.gateway.account.dto.AccountSearchResultDTO
uses gw.api.database.Query
uses edge.PlatformSupport.Bundle
uses edge.capabilities.helpers.pagination.dto.QueryOptionsDTO
uses edge.capabilities.helpers.pagination.dto.QueryParameterDTO
uses java.util.ArrayList
@JsonRpcRunAsInternalGWUser
  @JsonRpcMethod
  @ApidocMethodDescription("Returns a list of potentially existing accounts.")
  @ApidocAvailableSince("4.0")
  function getPotentialExistingAccounts(accountSearchCriteriaDTO: AccountSearchCriteriaDTO): AccountSearchResultDTO[] {
    if(!perm.System.searchaccounts) {
      throw new AuthorizationException(){:Message = "User is not authorized to search accounts"}
    }
    var anAccountSearchCriteria = _accountSearchPlugin.createSearchCriteria(accountSearchCriteriaDTO)

    if (accountSearchCriteriaDTO.ContactType == ContactType.TC_COMPANY) {
      /*If company name is greater than 5 characters then do an exact match*/
      anAccountSearchCriteria.CompanyNameExact = (anAccountSearchCriteria.CompanyName.length < 5)
    } else if (accountSearchCriteriaDTO.ContactType == ContactType.TC_PERSON) {
      anAccountSearchCriteria.LastNameExact = (anAccountSearchCriteria.LastName.length < 5)
      anAccountSearchCriteria.FirstNameExact = (anAccountSearchCriteria.FirstName.length < 5)
    }

    final var potentialAccounts = anAccountSearchCriteria.performSearch()*.Account

    return _accountPlugin.toSearchResultDTOArray(potentialAccounts)
  }

DefaultAccount plugin changes

Make the following changes to /gsrc/edge/capabilities/gateway/account/DefaultAccountPlugin.gs:

Original file content:
package edge.capabilities.gateway.account

uses edge.PlatformSupport.CurrencyPlatformUtil
uses edge.capabilities.gateway.account.dto.AccountDTO
uses edge.capabilities.gateway.contact.IContactPlugin
uses edge.capabilities.gateway.currency.local.ICurrencyPlugin
uses edge.capabilities.gateway.policy.IPolicySummaryPlugin
uses edge.di.annotations.ForAllGwNodes
uses java.lang.IllegalArgumentException
uses java.util.Date
uses edge.capabilities.gateway.billing.IAccountBillingSummaryPlugin
uses edge.capabilities.gateway.contact.dto.ContactBaseDTO
uses java.lang.Exception
uses edge.PlatformSupport.Reflection
uses edge.PlatformSupport.Logger
uses edge.capabilities.gateway.user.local.IProducerCodePlugin
uses edge.capabilities.helpers.GatewayJobUtil
uses edge.capabilities.gateway.claim.IClaimSummaryPlugin
uses edge.exception.EntityNotFoundException
uses gw.pl.currency.MonetaryAmount
uses gw.product.ProducerCodeSearchCriteria
uses edge.capabilities.policycommon.accountcontact.IAccountContactPlugin
override function toDTOArray(accounts: Account[]): AccountDTO[] {
    if (accounts != null && !accounts.IsEmpty){
      return accounts.map(\acc -> toDTO(acc))
    }
    return new AccountDTO[]{}
  }

override function updateAccount(anAccount: Account, dto: AccountDTO) {
    var accountHolder: ContactBaseDTO = null
    if (dto.AccountHolder != null) {
      _accContactPlugin.updateContact(anAccount.AccountHolderContact, dto.AccountHolder)
    }
  }
Updated file content:
package edge.capabilities.gateway.account

uses edge.PlatformSupport.CurrencyPlatformUtil
uses edge.capabilities.gateway.account.dto.AccountDTO
uses edge.capabilities.gateway.account.dto.AccountSearchResultDTO
uses edge.capabilities.gateway.account.dto.AccountSearchHolderDTO
uses edge.capabilities.address.dto.AddressDTO
uses edge.capabilities.gateway.contact.IContactPlugin
uses edge.capabilities.gateway.currency.local.ICurrencyPlugin
uses edge.capabilities.gateway.policy.IPolicySummaryPlugin
uses edge.di.annotations.ForAllGwNodes
uses java.lang.IllegalArgumentException
uses java.util.Date
uses edge.capabilities.gateway.billing.IAccountBillingSummaryPlugin
uses edge.capabilities.gateway.contact.dto.ContactBaseDTO
uses java.lang.Exception
uses edge.PlatformSupport.Reflection
uses edge.PlatformSupport.Logger
uses edge.capabilities.gateway.user.local.IProducerCodePlugin
uses edge.capabilities.helpers.GatewayJobUtil
uses edge.capabilities.gateway.claim.IClaimSummaryPlugin
uses edge.exception.EntityNotFoundException
uses gw.pl.currency.MonetaryAmount
uses gw.product.ProducerCodeSearchCriteria
uses edge.capabilities.policycommon.accountcontact.IAccountContactPlugin
  override function toDTOArray(accounts: Account[]): AccountDTO[] {
    if (accounts != null && !accounts.IsEmpty){
      return accounts.map(\acc -> toDTO(acc))
    }
    return new AccountDTO[]{}
  }

  override function toSearchResultDTOArray(accounts: Account[]): AccountSearchResultDTO[] {
    if (accounts == null || accounts.IsEmpty) {
      return new AccountSearchResultDTO[]{}
    }
    return accounts.map(\acc -> {
      final var dto = new AccountSearchResultDTO()
      dto.AccountNumber = acc.AccountNumber
      final var holder = new AccountSearchHolderDTO()
      holder.DisplayName = acc.AccountHolderContact?.DisplayName
      final var contactAddress = acc.AccountHolderContact?.PrimaryAddress
      if (contactAddress != null) {
        final var addr = new AddressDTO()
        addr.AddressLine1 = contactAddress.AddressLine1
        addr.City = contactAddress.City
        addr.State = contactAddress.State
        addr.PostalCode = contactAddress.PostalCode
        addr.Country = contactAddress.Country
        holder.PrimaryAddress = addr
      }
      dto.AccountHolder = holder
      return dto
    })
  }

  override function updateAccount(anAccount: Account, dto: AccountDTO) {
    var accountHolder: ContactBaseDTO = null
    if (dto.AccountHolder != null) {
      _accContactPlugin.updateContact(anAccount.AccountHolderContact, dto.AccountHolder)
    }
  }

IAccount plugin changes

Make the following changes to /gsrc/edge/capabilities/gateway/account/IAccountPlugin.gs‎:

Original file content:
package edge.capabilities.gateway.account

uses edge.capabilities.gateway.account.dto.AccountDTO

interface IAccountPlugin {

  public function toDTO(anAccount : Account) : AccountDTO
  public function toDTOArray(accounts : Account[]) : AccountDTO[]
  public function accountBaseDetailsToDTO(anAccount : Account) : AccountDTO
  public function accountBaseDetailsToDTOArray(accounts : Account[]) : AccountDTO[]
  public function updateAccount(anAccount : Account, dto : AccountDTO)
  public function createAccount(dto : AccountDTO) : Account

}
Updated file content:
package edge.capabilities.gateway.account

uses edge.capabilities.gateway.account.dto.AccountDTO
uses edge.capabilities.gateway.account.dto.AccountSearchResultDTO

interface IAccountPlugin {

  public function toDTO(anAccount : Account) : AccountDTO
  public function toDTOArray(accounts : Account[]) : AccountDTO[]
  public function accountBaseDetailsToDTO(anAccount : Account) : AccountDTO
  public function accountBaseDetailsToDTOArray(accounts : Account[]) : AccountDTO[]
  public function toSearchResultDTOArray(accounts : Account[]) : AccountSearchResultDTO[]
  public function updateAccount(anAccount : Account, dto : AccountDTO)
  public function createAccount(dto : AccountDTO) : Account

}

AccountSearchHolderDTO changes

In the /gsrc/edge/capabilities/gateway/account/dto/‎ directory, create a new file called AccountSearchHolderDTO.gs and add the following content:
package edge.capabilities.gateway.account.dto

uses edge.jsonmapper.JsonProperty
uses edge.capabilities.address.dto.AddressDTO

/**
 * Minimal account-holder summary for search results.
 * Contains only display name and primary address — no PII (DOB, licence, email, phone).
 */
class AccountSearchHolderDTO {

  @JsonProperty
  var _displayName : String as DisplayName

  @JsonProperty
  var _primaryAddress : AddressDTO as PrimaryAddress

}

AccountSearchResultDTO changes

In the /gsrc/edge/capabilities/gateway/account/dto/‎ directory, create a new file called AccountSearchResultDTO.gs‎ and add the following content:
package edge.capabilities.gateway.account.dto

uses edge.jsonmapper.JsonProperty

/**
 * Minimal DTO for account search results. Contains only the fields required to identify
 * an account in a list — account number and a stripped-down account holder (display name
 * and primary address only). Sensitive fields (DOB, licence, email, phone, billing data,
 * activity counts) are intentionally excluded.
 */
class AccountSearchResultDTO {

  @JsonProperty
  var _accountNumber : String as AccountNumber

  @JsonProperty
  var _accountHolder : AccountSearchHolderDTO as AccountHolder

}

CustomerEngage Quote and Buy API endpoint changes

InsuranceSuite application affected: PolicyCenter

Digital reference application affected: CustomerEngage Quote and Buy

Guidewire has modified the configuration of the CustomerEngage Quote and Buy /guidance/guidance API endpoint for improved security. There are steps that you can take to have improved security and need to manually add the changes to your own files.

The following files have been updated:
  • UserProfileHandler.gs

The following sections show the original and updated code. Bold lines in each code block indicate where the changes were made for this fix.

UserProfileHandler changes

Make the following changes to /gsrc/edge/capabilities/profileinfo/user/UserProfileHandler.gs:

Original file content:
@JsonRpcMethod
  @ApidocMethodDescription("When the user wishes to update their account contact information.")
  @ApidocAvailableSince("5.0")
  public function updateAccountContactSummary(newAccountSummaryDTO : AccountSummaryDTO) {
    final var account = AccountUtil.getAccountByAccountNumber(newAccountSummaryDTO.AccountNumber)
    AccountUtil.verifyAccountForEffectiveUser(_userProvider.EffectiveUser, account);

    var accountContact = newAccountSummaryDTO.AccountContact
    _accountPlugin.updateAccountContactSummary(account, accountContact)

    if(newAccountSummaryDTO.isBillingAddressSame) {
      _accountPlugin.updateAccountBillingSummary(account, newAccountSummaryDTO.AccountContact)
    } else {
      _accountPlugin.updateAccountBillingSummary(account, newAccountSummaryDTO.BillingContact)
    }
  }
}
Updated file content:
@JsonRpcMethod
  @ApidocMethodDescription("When the user wishes to update their account contact information.")
  @ApidocAvailableSince("5.0")
  public function updateAccountContactSummary(newAccountSummaryDTO : AccountSummaryDTO) {
    final var account = AccountUtil.getAccountByAccountNumber(newAccountSummaryDTO.AccountNumber, _userProvider.EffectiveUser)

    var accountContact = newAccountSummaryDTO.AccountContact
    _accountPlugin.updateAccountContactSummary(account, accountContact)

    if(newAccountSummaryDTO.isBillingAddressSame) {
      _accountPlugin.updateAccountBillingSummary(account, newAccountSummaryDTO.AccountContact)
    } else {
      _accountPlugin.updateAccountBillingSummary(account, newAccountSummaryDTO.BillingContact)
    }
  }
}

CustomerEngage Account Management improved access control

InsuranceSuite application affected: PolicyCenter

Digital reference application affected: CustomerEngage Account Management

Guidewire has modified the configuration of the CustomerEngage Account Management access control when creating a Personal Auto quote for improved security in quote submissions. There are steps that you can take to have improved security and need to manually add the changes to your own files. The changes need to be added to the following files:
  • /gsrc/edge/capabilities/quote/submission/GatewayQuoteHandler.gs‎
  • /gsrc/edge/capabilities/quote/helper/session/DefaultSessionPlugin.gs‎
  • /gsrc/edge/capabilities/quote/helper/session/ISessionPlugin.gs‎
  • /gsrc/edge/capabilities/quote/submission/QuoteHandler.gs‎
  • /gsrc/edge/capabilities/quote/submission/UnderwritingQuoteRetrievalHandler.gs‎
  • /gsrc/edge/capabilities/quote/submission/UnderwritingQuoteHandler.gs‎
  • /gsrc/edge/oauth/authplugin/EdgeAuthenticationSourceCreatorPlugin.gs‎
  • /gsrc/main/gsrc/edge/servlet/security/DefaultHttpRequestUserIdentityPlugin.gs‎
You must also update the following front-end files:
  • applications/common/capabilities-react/gw-capability-quoteandbind-ho-react/HOWizard.jsx
  • applications/common/capabilities-react/gw-capability-quoteandbind-pa-react/PAWizard.jsx

The following sections show the original and updated code. Bold lines in each code block indicate where the changes were made for this fix.

GatewayQuoteHandler changes

The following changes need to be made to /gsrc/edge/capabilities/quote/submission/GatewayQuoteHandler.gs‎:

Original file content:
package edge.capabilities.quote.submission

uses edge.capabilities.policycommon.accountcontact.IAccountContactPlugin
uses edge.capabilities.policycommon.validation.dto.PCValidationResultDTOMapper
uses edge.capabilities.policycommon.validation.dto.PCValidationResultsDTO
uses edge.capabilities.policyjob.quoting.exception.UnderwritingException
uses edge.capabilities.quote.submission.quoting.util.SubmissionUnderwritingIssuesUtil
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.annotation.JsonRpcRunAsInternalGWUser
uses edge.di.annotations.InjectableNode
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.el.Expr
uses edge.aspects.validation.annotations.Context
uses edge.security.authorization.IAuthorizerProviderPlugin
uses edge.capabilities.quote.helper.session.ISessionPlugin
uses edge.capabilities.quote.mailing.IQuoteMailingPlugin
uses edge.capabilities.quote.submission.lob.ILobMetadataPlugin
uses edge.capabilities.quote.submission.dto.QuoteDataDTO
uses edge.capabilities.quote.submission.dto.QuoteRetrievalDTO
uses edge.capabilities.policyjob.binding.IBindingPlugin
uses edge.capabilities.quote.submission.quoting.ISubmissionQuotePlugin
uses edge.capabilities.quote.submission.lob.IQuoteLobDataPlugin
uses edge.capabilities.quote.submission.base.IBaseSubmissionPlugin
uses edge.capabilities.quote.submission.lob.LobDTO
uses edge.capabilities.policyjob.lob.dto.LobCoveragesDTO
uses edge.capabilities.policyjob.lob.ILobCoveragesPlugin
uses edge.capabilities.policyjob.binding.dto.PaymentPlanDTO
uses edge.capabilities.quote.mailing.dto.QuoteEmailDTO
uses edge.capabilities.policycommon.validation.IEdgeValidationRulesPlugin
uses edge.capabilities.policycommon.validation.dto.UWIssueDTO
uses edge.capabilities.policycommon.validation.IUWIssuePlugin
uses edge.capabilities.policycommon.validation.dto.JobValidationUnderwritingIssuesDTO

class GatewayQuoteHandler extends QuoteHandler{


  @InjectableNode
  @Param("basePlugin", "Plugin used to manage base quote information")
  @Param("lobPlugin", "Plugin used to manage lob specific information")
  @Param("lobCoveragesUpdatePlugin", "Plugin used to update coverage information")
  @Param("sessionPlugin", "Session management plugin")
  @Param("quotingPlugin", "Quoting process plugin implementation")
  @Param("bindingPlugin", "Binding process plugin")
  @Param("quoteMailingPlugin", "Plugin used to mail the quote")
  @Param("lobMetadataPlugin", "Plugin used to extend metadata generation")
  @Param("authorizer", "Authorizer added to check user can retrieve submission")
  @Param("accContactPlugin", "Plugin used for creating and updating the account holder")
  @Param("validationPlugin", "Plugin used to perform entity validation")
  @Param("aUWIssuePlugin", "Plugin used to perform serialization of UW issues")
  construct(
      basePlugin : IBaseSubmissionPlugin,
      lobPlugin : IQuoteLobDataPlugin<LobDTO>,
      lobCoveragesUpdatePlugin : ILobCoveragesPlugin <LobCoveragesDTO>,
      sessionPlugin : ISessionPlugin,
      quotingPlugin : ISubmissionQuotePlugin,
      bindingPlugin : IBindingPlugin,
      quoteMailingPlugin : IQuoteMailingPlugin,
      lobMetadataPlugin : ILobMetadataPlugin,
      authorizer:IAuthorizerProviderPlugin,
      accContactPlugin: IAccountContactPlugin,
      validationPlugin : IEdgeValidationRulesPlugin,
      aUWIssuePlugin : IUWIssuePlugin) {

    super(
        basePlugin,
        lobPlugin,
        lobCoveragesUpdatePlugin,
        sessionPlugin,
        quotingPlugin,
        bindingPlugin,
        quoteMailingPlugin,
        lobMetadataPlugin,
        authorizer,
        accContactPlugin,
        validationPlugin,
        aUWIssuePlugin
    )
Updated file content:
package edge.capabilities.quote.submission

uses edge.capabilities.policycommon.accountcontact.IAccountContactPlugin
uses edge.capabilities.policycommon.validation.dto.PCValidationResultDTOMapper
uses edge.capabilities.policycommon.validation.dto.PCValidationResultsDTO
uses edge.capabilities.policyjob.quoting.exception.UnderwritingException
uses edge.capabilities.quote.submission.quoting.util.SubmissionUnderwritingIssuesUtil
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.annotation.JsonRpcRunAsInternalGWUser
uses edge.di.annotations.InjectableNode
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.el.Expr
uses edge.aspects.validation.annotations.Context
uses edge.security.authorization.IAuthorizerProviderPlugin
uses edge.capabilities.quote.helper.session.ISessionPlugin
uses edge.security.EffectiveUserProvider
uses edge.capabilities.quote.mailing.IQuoteMailingPlugin
uses edge.capabilities.quote.submission.lob.ILobMetadataPlugin
uses edge.capabilities.quote.submission.dto.QuoteDataDTO
uses edge.capabilities.quote.submission.dto.QuoteRetrievalDTO
uses edge.capabilities.policyjob.binding.IBindingPlugin
uses edge.capabilities.quote.submission.quoting.ISubmissionQuotePlugin
uses edge.capabilities.quote.submission.lob.IQuoteLobDataPlugin
uses edge.capabilities.quote.submission.base.IBaseSubmissionPlugin
uses edge.capabilities.quote.submission.lob.LobDTO
uses edge.capabilities.policyjob.lob.dto.LobCoveragesDTO
uses edge.capabilities.policyjob.lob.ILobCoveragesPlugin
uses edge.capabilities.policyjob.binding.dto.PaymentPlanDTO
uses edge.capabilities.quote.mailing.dto.QuoteEmailDTO
uses edge.capabilities.policycommon.validation.IEdgeValidationRulesPlugin
uses edge.capabilities.policycommon.validation.dto.UWIssueDTO
uses edge.capabilities.policycommon.validation.IUWIssuePlugin
uses edge.capabilities.policycommon.validation.dto.JobValidationUnderwritingIssuesDTO


class GatewayQuoteHandler extends QuoteHandler{


  @InjectableNode
  @Param("basePlugin", "Plugin used to manage base quote information")
  @Param("lobPlugin", "Plugin used to manage lob specific information")
  @Param("lobCoveragesUpdatePlugin", "Plugin used to update coverage information")
  @Param("sessionPlugin", "Session management plugin")
  @Param("quotingPlugin", "Quoting process plugin implementation")
  @Param("bindingPlugin", "Binding process plugin")
  @Param("quoteMailingPlugin", "Plugin used to mail the quote")
  @Param("lobMetadataPlugin", "Plugin used to extend metadata generation")
  @Param("authorizer", "Authorizer added to check user can retrieve submission")
  @Param("accContactPlugin", "Plugin used for creating and updating the account holder")
  @Param("validationPlugin", "Plugin used to perform entity validation")
  @Param("aUWIssuePlugin", "Plugin used to perform serialization of UW issues")
  @Param("aUserProvider", "Provider for the effective user, used to verify account ownership")
  construct(
      basePlugin : IBaseSubmissionPlugin,
      lobPlugin : IQuoteLobDataPlugin<LobDTO>,
      lobCoveragesUpdatePlugin : ILobCoveragesPlugin <LobCoveragesDTO>,
      sessionPlugin : ISessionPlugin,
      quotingPlugin : ISubmissionQuotePlugin,
      bindingPlugin : IBindingPlugin,
      quoteMailingPlugin : IQuoteMailingPlugin,
      lobMetadataPlugin : ILobMetadataPlugin,
      authorizer:IAuthorizerProviderPlugin,
      accContactPlugin: IAccountContactPlugin,
      validationPlugin : IEdgeValidationRulesPlugin,
      aUWIssuePlugin : IUWIssuePlugin,
      aUserProvider: EffectiveUserProvider) {

    super(
        basePlugin,
        lobPlugin,
        lobCoveragesUpdatePlugin,
        sessionPlugin,
        quotingPlugin,
        bindingPlugin,
        quoteMailingPlugin,
        lobMetadataPlugin,
        authorizer,
        accContactPlugin,
        validationPlugin,
        aUWIssuePlugin,
        aUserProvider
    )

DefaultSession plugin changes

The following changes need to be made to /gsrc/edge/capabilities/quote/helper/session/DefaultSessionPlugin.gs‎:

Original file content:
override function getSession(foreignId: String): String {
    final var sess = Bundle.resolveInTransaction(\bundle -> {
      final var mtSession = new PortalSession_MPExt()
      mtSession.foreignId = foreignId
      mtSession.sessionType = DEFAULT_SESSION_TYPE
      mtSession.sessionUUID = UUID.randomUUID().toString()
      mtSession.issueDate = DateUtil.currentDate()
      return mtSession
    })
    return sess.sessionUUID
  }
Updated file content:
override function getSession(foreignId: String): String {
    return getSession(foreignId, null)
  }

  override function getSession(foreignId: String, callerUsername: String): String {
    final var sess = Bundle.resolveInTransaction(\bundle -> {
      final var mtSession = new PortalSession_MPExt()
      mtSession.foreignId = foreignId
      mtSession.sessionType = DEFAULT_SESSION_TYPE
      mtSession.sessionUUID = UUID.randomUUID().toString()
      mtSession.issueDate = DateUtil.currentDate()
      mtSession.username = callerUsername
      return mtSession
    })
    return sess.sessionUUID
  }

ISession plugin changes

The following changes need to be made to /gsrc/edge/capabilities/quote/helper/session/ISessionPlugin.gs‎:

Original file content:
package edge.capabilities.quote.helper.session
uses java.lang.String
uses gw.lang.Returns

/**
 * Service used to work with quote sessions.
 */
interface ISessionPlugin {
  function getSession(foreignId : String):String
  
  /**
   * Validates and refreshes a session.
   */
  @Returns("<code>true</code> iff session was valid (session timeout should be updaded)." +
           "<code>false</code> iff session is not valid")
  function validateAndRefreshSession(sessionUUID : String, foreignId : String)
}
Updated file content:
package edge.capabilities.quote.helper.session
uses java.lang.String
uses gw.lang.Returns

/**
 * Service used to work with quote sessions.
 */
interface ISessionPlugin {
  function getSession(foreignId : String):String

  /**
   * Creates a session bound to a specific caller identity.
   * The callerUsername is stored so it can be verified at retrieval time.
   * Passing null is allowed (anonymous callers).
   */
  function getSession(foreignId : String, callerUsername : String) : String

  /**
   * Validates and refreshes a session.
   */
  @Returns("<code>true</code> iff session was valid (session timeout should be updaded)." +
           "<code>false</code> iff session is not valid")
  function validateAndRefreshSession(sessionUUID : String, foreignId : String)
}

QuoteHandler changes

The following changes need to be made to /gsrc/edge/capabilities/quote/submission/QuoteHandler.gs‎:

Original file content:
package edge.capabilities.quote.submission

uses edge.jsonrpc.annotation.CaptchaCheck
uses edge.capabilities.policycommon.accountcontact.IAccountContactPlugin
uses edge.capabilities.policycommon.availability.ProductCodeUtil
uses edge.capabilities.policycommon.validation.IEdgeValidationRulesPlugin
uses edge.capabilities.policycommon.validation.IUWIssuePlugin
uses edge.capabilities.policycommon.validation.dto.JobValidationUnderwritingIssuesDTO
uses edge.capabilities.policycommon.validation.dto.PCValidationResultDTOMapper
uses edge.capabilities.policycommon.validation.dto.PCValidationResultsDTO
uses edge.capabilities.policycommon.validation.dto.UWIssueDTO
uses edge.capabilities.quote.submission.quoting.util.SubmissionUnderwritingIssuesUtil
uses edge.di.annotations.InjectableNode
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.AbstractRpcHandler
uses edge.PlatformSupport.Bundle
uses edge.PlatformSupport.Logger
uses edge.PlatformSupport.Reflection
uses edge.el.Expr
uses edge.aspects.validation.annotations.Context
uses gw.api.database.Query
uses edge.security.authorization.IAuthorizerProviderPlugin
uses edge.security.authorization.Authorizer
uses edge.exception.EntityNotFoundException
uses edge.jsonrpc.annotation.JsonRpcUnauthenticatedMethod
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.jsonrpc.exception.JsonRpcSecurityException

uses edge.capabilities.quote.helper.session.ISessionPlugin
uses edge.capabilities.quote.helper.questionset.util.QuestionSetUtil
uses edge.capabilities.quote.mailing.IQuoteMailingPlugin
uses edge.capabilities.quote.mailing.dto.QuoteEmailDTO
uses edge.webapimodel.dto.PCWebApiModelDTO
uses edge.capabilities.quote.submission.lob.ILobMetadataPlugin
uses edge.capabilities.quote.submission.dto.QuoteDataDTO
uses edge.capabilities.quote.submission.dto.QuoteRetrievalDTO
uses edge.capabilities.policyjob.binding.IBindingPlugin

uses edge.capabilities.quote.submission.quoting.ISubmissionQuotePlugin
uses edge.capabilities.quote.submission.base.IBaseSubmissionPlugin
uses edge.capabilities.policyjob.quoting.exception.UnderwritingException
uses edge.capabilities.quote.submission.quoting.exception.EntityValidationException
uses edge.capabilities.quote.submission.lob.LobDTO
uses edge.capabilities.quote.submission.quoting.exception.BlockQuoteUnderwritingException
uses edge.capabilities.policyjob.lob.dto.LobCoveragesDTO
uses java.lang.IllegalArgumentException
uses edge.capabilities.policyjob.lob.ILobCoveragesPlugin
uses edge.capabilities.quote.submission.lob.IQuoteLobDataPlugin
uses edge.capabilities.policyjob.quoting.util.QuoteUtil
uses edge.capabilities.policyjob.binding.dto.PaymentPlanDTO
uses edge.capabilities.quote.submission.util.SubmissionUtil
uses gw.util.Pair


class QuoteHandler extends AbstractRpcHandler {

  private static final var LOGGER = new Logger(Reflection.getRelativeName(QuoteHandler))

  /**
   * Validation Rules Plugin
   */
  protected var _validationPlugin : IEdgeValidationRulesPlugin

  /**
   * Used to map underwriting issues and approve/refer issues if permissions allow.
   */
  protected var _uwIssuePlugin: IUWIssuePlugin

  /**
   * A plugin to manage account contacts
   */
  private var _accContactPlugin: IAccountContactPlugin

  /**
   * Base submission plugin used in quote.
   */
  private var _basePlugin: IBaseSubmissionPlugin

  /**
   * Plugin to process LOB specific data.
   */
  protected var _lobPlugin: IQuoteLobDataPlugin<LobDTO>

  /**
   * Plugin to process LOB coverage data.
   */
  private var _lobCoveragesUpdatePlugin: ILobCoveragesPlugin<LobCoveragesDTO>


  /**
   * Session management plugin.
   */
  protected var _sessionPlugin: ISessionPlugin


  /**
   * Quoting plugin.
   */
  protected var _quotingPlugin: ISubmissionQuotePlugin


  /**
   * Submission binding plugin.
   */
  private var _bindingPlugin: IBindingPlugin


  /**
   * Quote mailing plugin.
   */
  private var _quoteMailingPlugin: IQuoteMailingPlugin


  /**
   * Metadata generation extension plugin.
   */
  private var _lobMetadataPlugin: ILobMetadataPlugin

  /**
   * Authorizer
   */
  private var _submissionAuthorizer: Authorizer<Submission>as readonly SubmissionAuthorizer

  construct() { }

  @InjectableNode
  @Param("basePlugin", "Plugin used to manage base quote information")
  @Param("lobPlugin", "Plugin used to manage lob specific information")
  @Param("lobCoveragesUpdatePlugin", "Plugin used to update coverage information")
  @Param("sessionPlugin", "Session management plugin")
  @Param("quotingPlugin", "Quoting process plugin implementation")
  @Param("bindingPlugin", "Binding process plugin")
  @Param("quoteMailingPlugin", "Plugin used to mail the quote")
  @Param("lobMetadataPlugin", "Plugin used to extend metadata generation")
  @Param("authorizer", "Authorizer added to check user can retrieve submission")
  @Param("accContactPlugin", "Plugin used for creating and updating the account holder")
  @Param("validationPlugin", "Plugin used to perform entity validation")
  @Param("aUWIssuePlugin", "Plugin used to perform serialization of UW issues")
  construct(
      basePlugin: IBaseSubmissionPlugin,
      lobPlugin: IQuoteLobDataPlugin<LobDTO>,
      lobCoveragesUpdatePlugin: ILobCoveragesPlugin<LobCoveragesDTO>,
      sessionPlugin: ISessionPlugin,
      quotingPlugin: ISubmissionQuotePlugin,
      bindingPlugin: IBindingPlugin,
      quoteMailingPlugin: IQuoteMailingPlugin,
      lobMetadataPlugin: ILobMetadataPlugin,
      authorizer: IAuthorizerProviderPlugin,
      accContactPlugin: IAccountContactPlugin,
      validationPlugin : IEdgeValidationRulesPlugin,
      aUWIssuePlugin : IUWIssuePlugin
  ) {
    this._basePlugin = basePlugin
    this._lobPlugin = lobPlugin
    this._lobCoveragesUpdatePlugin = lobCoveragesUpdatePlugin
    this._sessionPlugin = sessionPlugin
    this._quotingPlugin = quotingPlugin
    this._bindingPlugin = bindingPlugin
    this._quoteMailingPlugin = quoteMailingPlugin
    this._lobMetadataPlugin = lobMetadataPlugin
    this._submissionAuthorizer = authorizer.authorizerFor(Submission)
    this._accContactPlugin = accContactPlugin
    this._validationPlugin = validationPlugin
    this._uwIssuePlugin = aUWIssuePlugin
  }

  @JsonRpcUnauthenticatedMethod
  @ApidocMethodDescription("Retrieves the meta data required for the quoting handler, including details such as display keys and question sets.")
  @ApidocAvailableSince("5.0")
  function getMetaData(): Object {
    return PCWebApiModelDTO.forTypes(
        {QuoteDataDTO, QuoteEmailDTO, QuoteRetrievalDTO},
        _lobMetadataPlugin.getQuestionSetCodes().map(\qs -> QuestionSetUtil.getQuestionSetByCode(qs))
    )
  }


  /**
   * Creates new submission job and generates a session to be used during the submission process.
   *
   * <dl>
   * <dt>Calls:</dt>
   * <dd><code>IBaseSubmissionPlugin#createSubmission(String,QuoteBaseDataDTO)</code> -
   * to create a new submission passing the base data in <code>qdd.BaseData</code></dd>
   * <dd><code>ILobDataPlugin#updateNewSubmission(PolicyPeriod)</code> - to update the LOB coverables on the base period</dd>
   * <dd><code>ISessionPlugin#getSession(String)</code> - to create the session id which will be added to the
   * returned dto</dd>
   * <dd><code>IBaseSubmissionPlugin#toDTO(Submission)</code> - populates QuoteDataDTO.BaseData, draft data common across LOBs</dd>
   * <dd><code>ILobDataPlugin#toDTO(Submission)</code> - populates QuoteDataDTO.LobDataDTO, draft data specific to LOBs</dd>
   * </dl>
   *
   * @param qdd initial data to create the submission. This implementation only uses the information in <code>qdd.BaseData</code>
   *            and <code>qdd.Lobs</code>
   * @return the DTO for the newly created submission. The contents of the returned value are as follows:
   * <dl>
   * <dt>SessionUUID</dt><dd>the session ID returned by the session plugin</dd>
   * <dt>QuoteID</dt><dd>the JobNumber of the newly created submission</dd>
   * <dt>BaseData</dt><dd>data common across LOBs</dd>
   * <dt>LobDataDTO</dt><dd>LOB coverables and coverages</dd>
   * <dt>QuotingData</dt><dd><code>null</code></dd>
   * <dt>BindingData</dt><dd><code>null</code></dd>
   * <dt>IsSubmitAgent</dt><dd><code>null</code></dd>
   * </dl>
   */
  @JsonRpcUnauthenticatedMethod
  @ApidocMethodDescription("Creates new submission job and generates a session to be used during the submission process.")
  @ApidocAvailableSince("4.0")
  public function create(qdd: QuoteDataDTO): QuoteDataDTO {
    final var draftSubmission = Bundle.resolveInTransaction(\b -> {
      final var versionSpecificProductCode = ProductCodeUtil.getVersionSpecificProductCode(qdd.BaseData.ProductCode)
      var aSubmission = _basePlugin.createSubmission(versionSpecificProductCode, qdd.BaseData)
      _lobPlugin.updateFromDTO(aSubmission.SelectedVersion, qdd.LobData, true)
      return aSubmission
    })
    final var sessId = _sessionPlugin.getSession(draftSubmission.JobNumber)
    return toDTOBaseData(sessId, draftSubmission)
  }

  /**
   * Creates new submission job for an account and generates a session to be used during the submission process. It does not update
   * the account details.
   *
   * <dl>
   * <dt>Calls:</dt>
   * <dd><code>create(qdd : QuoteDataDTO)</code> -
   * to create a new submission passing the data in <code>qdd</code>. </dd>
   * <dd><code>ILobDataPlugin#updateNewSubmission(PolicyPeriod)</code> - to update the LOB coverables on the base period</dd>
   * <dd><code>ISessionPlugin#getSession(String)</code> - to create the session id which will be added to the
   * returned dto</dd>
   * <dd><code>IBaseSubmissionPlugin#toDTO(Submission)</code> - populates QuoteDataDTO.BaseData, draft data common across LOBs</dd>
   * <dd><code>ILobDataPlugin#toDTO(Submission)</code> - populates QuoteDataDTO.LobDataDTO, draft data specific to LOBs</dd>
   * </dl>
   *
   * @param qdd initial data to create the submission. This implementation only uses the information in <code>qdd.BaseData</code>
   *            and <code>qdd.Lobs</code>
   * @return the DTO for the newly created submission. The contents of the returned value are as follows:
   * <dl>
   * <dt>SessionUUID</dt><dd>the session ID returned by the session plugin</dd>
   * <dt>QuoteID</dt><dd>the JobNumber of the newly created submission</dd>
   * <dt>BaseData</dt><dd>draft data common across LOBs</dd>
   * <dt>LobDataDTO</dt><dd>LOB coverables and coverages</dd>
   * <dt>QuotingData</dt><dd><code>null</code></dd>
   * <dt>BindingData</dt><dd><code>null</code></dd>
   * <dt>IsSubmitAgent</dt><dd><code>null</code></dd>
   * </dl>
   */
  @JsonRpcMethod
  @ApidocMethodDescription("Creates new submission job for an account and generates a session to be used during the submission process. It does not update the account detail.")
  @ApidocAvailableSince("4.0")
  public function createForAccount(qdd: QuoteDataDTO): QuoteDataDTO {
    if (qdd.BaseData == null || qdd.BaseData.AccountNumber == null) {
      throw new IllegalArgumentException("Account Number is required.")
    }

    return Bundle.resolveInTransaction(\bundle -> {
      var existingAccount = bundle.add(Account.finder.findAccountByAccountNumber(qdd.BaseData.AccountNumber))

      qdd.BaseData.AccountHolder = _accContactPlugin.toDTO(existingAccount.AccountHolderContact)
      return create(qdd)
    })
  }
/**
   * Retrieves an account submission
   *
   * <dl>
   * <dt>Calls:</dt>
   * <dd><code>retrieve(QuoteRetrievalDTO)</code> - to retrieve the account submission</dd>
   * <dt>Throws:</dt>
   * <dd><code>EntityNotFoundException</code> - if no submission can be found with a matching postal code</dd>
   * </dl>
   *
   * @param qrd data that captures the information needed to retrieve a quote
   * @return the DTO for the updated submission. The contents of the returned value are as follows:
   * <dl>
   * <dt>SessionUUID</dt><dd>the session ID</dd>
   * <dt>QuoteID</dt><dd>the JobNumber of the submission</dd>
   * <dt>BaseData</dt><dd>draft data common across LOBs</dd>
   * <dt>LobDataDTO</dt><dd>LOB coverables and coverages</dd>
   * <dt>QuotingData</dt><dd>the quote data</dd>
   * <dt>BindingData</dt><dd>the bind data</dd>
   * <dt>IsSubmitAgent</dt><dd><code>null</code></dd>
   * </dl>
   */
  @JsonRpcUnauthenticatedMethod
  @ApidocMethodDescription("Retrieves an account submission")
  @ApidocAvailableSince("2021.11.0")
  public function retrieveAccountSubmission(qrd: QuoteRetrievalDTO): QuoteDataDTO {
    final var DEFAULT_EXPIRY_HOURS: int = 1
    final var DEFAULT_SESSION_TYPE: String = "Submission"

    if (!qrd.sessionUUID.HasContent) {
       throw new JsonRpcSecurityException(){:Message = "Invalid session"}
    }

    final var results = Query.make(PortalSession_MPExt).compare("sessionUUID", Equals, qrd.sessionUUID).select()
    if(results.Count != 1) {
        throw new JsonRpcSecurityException(){:Message = "Invalid session"}
    }

    final var session = results.FirstResult

    if (!DEFAULT_SESSION_TYPE.equals(session.sessionType)) {
        throw new JsonRpcSecurityException(){:Message = "Invalid session"}
    }

    if (!gw.api.util.DateUtil.addHours(session.issueDate, DEFAULT_EXPIRY_HOURS).after( Date.CurrentDate)) {
        throw new JsonRpcSecurityException(){:Message = "Invalid session"}
    }

    return this.retrieve(qrd)
  }
Updated file content:
package edge.capabilities.quote.submission

uses edge.jsonrpc.annotation.CaptchaCheck
uses edge.capabilities.policycommon.accountcontact.IAccountContactPlugin
uses edge.capabilities.policycommon.availability.ProductCodeUtil
uses edge.capabilities.policycommon.validation.IEdgeValidationRulesPlugin
uses edge.capabilities.policycommon.validation.IUWIssuePlugin
uses edge.capabilities.policycommon.validation.dto.JobValidationUnderwritingIssuesDTO
uses edge.capabilities.policycommon.validation.dto.PCValidationResultDTOMapper
uses edge.capabilities.policycommon.validation.dto.PCValidationResultsDTO
uses edge.capabilities.policycommon.validation.dto.UWIssueDTO
uses edge.capabilities.quote.submission.quoting.util.SubmissionUnderwritingIssuesUtil
uses edge.di.annotations.InjectableNode
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.AbstractRpcHandler
uses edge.PlatformSupport.Bundle
uses edge.PlatformSupport.Logger
uses edge.PlatformSupport.Reflection
uses edge.el.Expr
uses edge.aspects.validation.annotations.Context
uses gw.api.database.Query
uses edge.security.authorization.IAuthorizerProviderPlugin
uses edge.security.authorization.Authorizer
uses edge.exception.EntityNotFoundException
uses edge.jsonrpc.annotation.JsonRpcUnauthenticatedMethod
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.jsonrpc.exception.JsonRpcSecurityException

uses edge.capabilities.quote.helper.session.ISessionPlugin
uses edge.capabilities.quote.helper.questionset.util.QuestionSetUtil
uses edge.capabilities.quote.mailing.IQuoteMailingPlugin
uses edge.capabilities.quote.mailing.dto.QuoteEmailDTO
uses edge.webapimodel.dto.PCWebApiModelDTO
uses edge.capabilities.quote.submission.lob.ILobMetadataPlugin
uses edge.capabilities.quote.submission.dto.QuoteDataDTO
uses edge.capabilities.quote.submission.dto.QuoteRetrievalDTO
uses edge.capabilities.policyjob.binding.IBindingPlugin

uses edge.capabilities.quote.submission.quoting.ISubmissionQuotePlugin
uses edge.capabilities.quote.submission.base.IBaseSubmissionPlugin
uses edge.capabilities.policyjob.quoting.exception.UnderwritingException
uses edge.capabilities.quote.submission.quoting.exception.EntityValidationException
uses edge.capabilities.quote.submission.lob.LobDTO
uses edge.capabilities.quote.submission.quoting.exception.BlockQuoteUnderwritingException
uses edge.capabilities.policyjob.lob.dto.LobCoveragesDTO
uses java.lang.IllegalArgumentException
uses edge.capabilities.policyjob.lob.ILobCoveragesPlugin
uses edge.capabilities.quote.submission.lob.IQuoteLobDataPlugin
uses edge.capabilities.policyjob.quoting.util.QuoteUtil
uses edge.capabilities.policyjob.binding.dto.PaymentPlanDTO
uses edge.capabilities.quote.submission.util.SubmissionUtil
uses edge.security.EffectiveUserProvider
uses edge.security.authorization.AuthorityType
uses gw.util.Pair

class QuoteHandler extends AbstractRpcHandler {

  private static final var LOGGER = new Logger(Reflection.getRelativeName(QuoteHandler))

  /**
   * Validation Rules Plugin
   */
  protected var _validationPlugin : IEdgeValidationRulesPlugin

  /**
   * Used to map underwriting issues and approve/refer issues if permissions allow.
   */
  protected var _uwIssuePlugin: IUWIssuePlugin

  /**
   * A plugin to manage account contacts
   */
  private var _accContactPlugin: IAccountContactPlugin

  /**
   * Base submission plugin used in quote.
   */
  private var _basePlugin: IBaseSubmissionPlugin

  /**
   * Plugin to process LOB specific data.
   */
  protected var _lobPlugin: IQuoteLobDataPlugin<LobDTO>

  /**
   * Plugin to process LOB coverage data.
   */
  private var _lobCoveragesUpdatePlugin: ILobCoveragesPlugin<LobCoveragesDTO>


  /**
   * Session management plugin.
   */
  protected var _sessionPlugin: ISessionPlugin


  /**
   * Quoting plugin.
   */
  protected var _quotingPlugin: ISubmissionQuotePlugin


  /**
   * Submission binding plugin.
   */
  private var _bindingPlugin: IBindingPlugin


  /**
   * Quote mailing plugin.
   */
  private var _quoteMailingPlugin: IQuoteMailingPlugin


  /**
   * Metadata generation extension plugin.
   */
  private var _lobMetadataPlugin: ILobMetadataPlugin

  /**
   * Authorizer
   */
  private var _submissionAuthorizer: Authorizer<Submission>as readonly SubmissionAuthorizer

  /**
   * Provider for the effective (JWT-authenticated) user, used to verify account ownership.
   */
  protected var _userProvider: EffectiveUserProvider

  construct() { }

  @InjectableNode
  @Param("basePlugin", "Plugin used to manage base quote information")
  @Param("lobPlugin", "Plugin used to manage lob specific information")
  @Param("lobCoveragesUpdatePlugin", "Plugin used to update coverage information")
  @Param("sessionPlugin", "Session management plugin")
  @Param("quotingPlugin", "Quoting process plugin implementation")
  @Param("bindingPlugin", "Binding process plugin")
  @Param("quoteMailingPlugin", "Plugin used to mail the quote")
  @Param("lobMetadataPlugin", "Plugin used to extend metadata generation")
  @Param("authorizer", "Authorizer added to check user can retrieve submission")
  @Param("accContactPlugin", "Plugin used for creating and updating the account holder")
  @Param("validationPlugin", "Plugin used to perform entity validation")
  @Param("aUWIssuePlugin", "Plugin used to perform serialization of UW issues")
  @Param("aUserProvider", "Provider for the effective user, used to verify account ownership")
  construct(
      basePlugin: IBaseSubmissionPlugin,
      lobPlugin: IQuoteLobDataPlugin<LobDTO>,
      lobCoveragesUpdatePlugin: ILobCoveragesPlugin<LobCoveragesDTO>,
      sessionPlugin: ISessionPlugin,
      quotingPlugin: ISubmissionQuotePlugin,
      bindingPlugin: IBindingPlugin,
      quoteMailingPlugin: IQuoteMailingPlugin,
      lobMetadataPlugin: ILobMetadataPlugin,
      authorizer: IAuthorizerProviderPlugin,
      accContactPlugin: IAccountContactPlugin,
      validationPlugin : IEdgeValidationRulesPlugin,
      aUWIssuePlugin : IUWIssuePlugin,
      aUserProvider: EffectiveUserProvider
  ) {
    this._basePlugin = basePlugin
    this._lobPlugin = lobPlugin
    this._lobCoveragesUpdatePlugin = lobCoveragesUpdatePlugin
    this._sessionPlugin = sessionPlugin
    this._quotingPlugin = quotingPlugin
    this._bindingPlugin = bindingPlugin
    this._quoteMailingPlugin = quoteMailingPlugin
    this._lobMetadataPlugin = lobMetadataPlugin
    this._submissionAuthorizer = authorizer.authorizerFor(Submission)
    this._accContactPlugin = accContactPlugin
    this._validationPlugin = validationPlugin
    this._uwIssuePlugin = aUWIssuePlugin
    this._userProvider = aUserProvider
  }

@JsonRpcUnauthenticatedMethod
  @ApidocMethodDescription("Retrieves the meta data required for the quoting handler, including details such as display keys and question sets.")
  @ApidocAvailableSince("5.0")
  function getMetaData(): Object {
    return PCWebApiModelDTO.forTypes(
        {QuoteDataDTO, QuoteEmailDTO, QuoteRetrievalDTO},
        _lobMetadataPlugin.getQuestionSetCodes().map(\qs -> QuestionSetUtil.getQuestionSetByCode(qs))
    )
  }


  /**
   * Creates new submission job and generates a session to be used during the submission process.
   *
   * <dl>
   * <dt>Calls:</dt>
   * <dd><code>IBaseSubmissionPlugin#createSubmission(String,QuoteBaseDataDTO)</code> -
   * to create a new submission passing the base data in <code>qdd.BaseData</code></dd>
   * <dd><code>ILobDataPlugin#updateNewSubmission(PolicyPeriod)</code> - to update the LOB coverables on the base period</dd>
   * <dd><code>ISessionPlugin#getSession(String)</code> - to create the session id which will be added to the
   * returned dto</dd>
   * <dd><code>IBaseSubmissionPlugin#toDTO(Submission)</code> - populates QuoteDataDTO.BaseData, draft data common across LOBs</dd>
   * <dd><code>ILobDataPlugin#toDTO(Submission)</code> - populates QuoteDataDTO.LobDataDTO, draft data specific to LOBs</dd>
   * </dl>
   *
   * @param qdd initial data to create the submission. This implementation only uses the information in <code>qdd.BaseData</code>
   *            and <code>qdd.Lobs</code>
   * @return the DTO for the newly created submission. The contents of the returned value are as follows:
   * <dl>
   * <dt>SessionUUID</dt><dd>the session ID returned by the session plugin</dd>
   * <dt>QuoteID</dt><dd>the JobNumber of the newly created submission</dd>
   * <dt>BaseData</dt><dd>data common across LOBs</dd>
   * <dt>LobDataDTO</dt><dd>LOB coverables and coverages</dd>
   * <dt>QuotingData</dt><dd><code>null</code></dd>
   * <dt>BindingData</dt><dd><code>null</code></dd>
   * <dt>IsSubmitAgent</dt><dd><code>null</code></dd>
   * </dl>
   */
  @JsonRpcUnauthenticatedMethod
  @ApidocMethodDescription("Creates new submission job and generates a session to be used during the submission process.")
  @ApidocAvailableSince("4.0")
  public function create(qdd: QuoteDataDTO): QuoteDataDTO {
    final var draftSubmission = Bundle.resolveInTransaction(\b -> {
      final var versionSpecificProductCode = ProductCodeUtil.getVersionSpecificProductCode(qdd.BaseData.ProductCode)
      var aSubmission = _basePlugin.createSubmission(versionSpecificProductCode, qdd.BaseData)
      _lobPlugin.updateFromDTO(aSubmission.SelectedVersion, qdd.LobData, true)
      return aSubmission
    })
    // Sessions created at quote creation time are not bound to a specific caller because
    // they may be created by an agent on behalf of a customer, or shared via URL.
    // Caller binding is applied later when the customer resumes the quote via retrieve().
    final var sessId = _sessionPlugin.getSession(draftSubmission.JobNumber)
    return toDTOBaseData(sessId, draftSubmission)
  }

  /**
   * Creates new submission job for an account and generates a session to be used during the submission process. It does not update
   * the account details.
   *
   * <dl>
   * <dt>Calls:</dt>
   * <dd><code>create(qdd : QuoteDataDTO)</code> -
   * to create a new submission passing the data in <code>qdd</code>. </dd>
   * <dd><code>ILobDataPlugin#updateNewSubmission(PolicyPeriod)</code> - to update the LOB coverables on the base period</dd>
   * <dd><code>ISessionPlugin#getSession(String)</code> - to create the session id which will be added to the
   * returned dto</dd>
   * <dd><code>IBaseSubmissionPlugin#toDTO(Submission)</code> - populates QuoteDataDTO.BaseData, draft data common across LOBs</dd>
   * <dd><code>ILobDataPlugin#toDTO(Submission)</code> - populates QuoteDataDTO.LobDataDTO, draft data specific to LOBs</dd>
   * </dl>
   *
   * @param qdd initial data to create the submission. This implementation only uses the information in <code>qdd.BaseData</code>
   *            and <code>qdd.Lobs</code>
   * @return the DTO for the newly created submission. The contents of the returned value are as follows:
   * <dl>
   * <dt>SessionUUID</dt><dd>the session ID returned by the session plugin</dd>
   * <dt>QuoteID</dt><dd>the JobNumber of the newly created submission</dd>
   * <dt>BaseData</dt><dd>draft data common across LOBs</dd>
   * <dt>LobDataDTO</dt><dd>LOB coverables and coverages</dd>
   * <dt>QuotingData</dt><dd><code>null</code></dd>
   * <dt>BindingData</dt><dd><code>null</code></dd>
   * <dt>IsSubmitAgent</dt><dd><code>null</code></dd>
   * </dl>
   */
  @JsonRpcMethod
  @ApidocMethodDescription("Creates new submission job for an account and generates a session to be used during the submission process. It does not update the account detail.")
  @ApidocAvailableSince("4.0")
  public function createForAccount(qdd: QuoteDataDTO): QuoteDataDTO {
    if (qdd.BaseData == null || qdd.BaseData.AccountNumber == null) {
      throw new IllegalArgumentException("Account Number is required.")
    }

    final var result = Bundle.resolveInTransaction(\bundle -> {
      var existingAccount = bundle.add(Account.finder.findAccountByAccountNumber(qdd.BaseData.AccountNumber))

      qdd.BaseData.AccountHolder = _accContactPlugin.toDTO(existingAccount.AccountHolderContact)
      return create(qdd)
    })
    // Bind the newly-created session to the account number in the DB so that
    // retrieveAccountSubmission can perform a server-side (non-forgeable) ownership check.
    Bundle.transaction(\bundle -> {
      final var sessResults = Query.make(PortalSession_MPExt).compare("sessionUUID", Equals, result.SessionUUID).select()
      if (sessResults.Count == 1) {
        final var sess = bundle.add(sessResults.FirstResult)
        sess.username = qdd.BaseData.AccountNumber
      }
    })
    return result
  }
/**
   * Retrieves an account submission
   *
   * <dl>
   * <dt>Calls:</dt>
   * <dd><code>retrieve(QuoteRetrievalDTO)</code> - to retrieve the account submission</dd>
   * <dt>Throws:</dt>
   * <dd><code>EntityNotFoundException</code> - if no submission can be found with a matching postal code</dd>
   * </dl>
   *
   * @param qrd data that captures the information needed to retrieve a quote
   * @return the DTO for the updated submission. The contents of the returned value are as follows:
   * <dl>
   * <dt>SessionUUID</dt><dd>the session ID</dd>
   * <dt>QuoteID</dt><dd>the JobNumber of the submission</dd>
   * <dt>BaseData</dt><dd>draft data common across LOBs</dd>
   * <dt>LobDataDTO</dt><dd>LOB coverables and coverages</dd>
   * <dt>QuotingData</dt><dd>the quote data</dd>
   * <dt>BindingData</dt><dd>the bind data</dd>
   * <dt>IsSubmitAgent</dt><dd><code>null</code></dd>
   * </dl>
   */
  @JsonRpcMethod
  @ApidocMethodDescription("Retrieves an account submission")
  @ApidocAvailableSince("2021.11.0")
  public function retrieveAccountSubmission(qrd: QuoteRetrievalDTO): QuoteDataDTO {
    final var effectiveUser = _userProvider.EffectiveUser
    final var DEFAULT_EXPIRY_HOURS: int = 1
    final var DEFAULT_SESSION_TYPE: String = "Submission"

    if (!qrd.sessionUUID.HasContent) {
       throw new JsonRpcSecurityException(){:Message = "Invalid session"}
    }

    final var results = Query.make(PortalSession_MPExt).compare("sessionUUID", Equals, qrd.sessionUUID).select()
    if(results.Count != 1) {
        throw new JsonRpcSecurityException(){:Message = "Invalid session"}
    }

    final var session = results.FirstResult

    // Require a JWT with an ACCOUNT authority that matches this session's account number.
    // The QnB frontend always sends Authorization: Bearer for logged-in users, so
    // effectiveUser carries grantedAuthorities (e.g. "guidewire.edge.account.C000143542.all").
    // Access is denied when:
    //   - the caller is not authenticated (no JWT → no account authorities), or
    //   - the caller's JWT account does not match the session's account (cross-account URL attack).
    if (session.username.HasContent) {
      final var jwtAccountNumbers = effectiveUser?.getTargets(AuthorityType.ACCOUNT)
      if (jwtAccountNumbers == null || jwtAccountNumbers.isEmpty()
          || !jwtAccountNumbers.contains(session.username)) {
        throw new JsonRpcSecurityException(){:Message = "Invalid session"}
      }
    }

   if (!session.foreignId.equals(qrd.QuoteID)) {
      throw new JsonRpcSecurityException(){:Message = "Invalid session"}
    }

    if (!DEFAULT_SESSION_TYPE.equals(session.sessionType)) {
        throw new JsonRpcSecurityException(){:Message = "Invalid session"}
    }

    if (!gw.api.util.DateUtil.addHours(session.issueDate, DEFAULT_EXPIRY_HOURS).after( Date.CurrentDate)) {
        throw new JsonRpcSecurityException(){:Message = "Invalid session"}
    }

    return this.retrieve(qrd)
  }

UnderwritingQuoteRetrievalHandler changes

The following changes need to be made to /gsrc/edge/capabilities/quote/submission/UnderwritingQuoteRetrievalHandler.gs

Original file content:
package edge.capabilities.quote.submission

uses edge.capabilities.gateway.job.submission.ISubmissionRetrievalPlugin
uses edge.capabilities.gateway.policy.IPolicyPeriodRetrievalPlugin
uses edge.capabilities.gateway.policy.IUnderwritingPolicyPeriodPlugin
uses edge.capabilities.industrycode.IIndustryCodePlugin
uses edge.capabilities.policycommon.accountcontact.IAccountContactPlugin
uses edge.capabilities.policycommon.validation.IEdgeValidationRulesPlugin
uses edge.capabilities.policycommon.validation.IUWIssuePlugin
uses edge.capabilities.policyjob.binding.IBindingPlugin
uses edge.capabilities.policyjob.binding.IUnderwritingBindingPlugin
uses edge.capabilities.policyjob.binding.IUnderwritingPaymentPlanPlugin
uses edge.capabilities.policyjob.binding.dto.PaymentPlanDTO
uses edge.capabilities.policyjob.lob.ILobCoveragesPlugin
uses edge.capabilities.policyjob.lob.dto.LobCoveragesDTO
uses edge.capabilities.quote.helper.session.ISessionPlugin
uses edge.capabilities.quote.mailing.IQuoteMailingPlugin
uses edge.capabilities.quote.submission.base.IBaseSubmissionPlugin
uses edge.capabilities.quote.submission.base.IUnderwritingBaseSubmissionPlugin
uses edge.capabilities.quote.submission.dto.QuoteDataDTO
uses edge.capabilities.quote.submission.dto.QuoteRetrievalDTO
uses edge.capabilities.quote.submission.issuing.IIssuePlugin
uses edge.capabilities.quote.submission.lob.ILobMetadataPlugin
uses edge.capabilities.quote.submission.lob.IQuoteLobDataPlugin
uses edge.capabilities.quote.submission.lob.LobDTO
uses edge.capabilities.quote.submission.quotedocument.IQuoteDocumentPlugin
uses edge.capabilities.quote.submission.quoting.ISubmissionQuotePlugin
uses edge.di.annotations.InjectableNode
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.annotation.JsonRpcUnauthenticatedMethod
uses edge.security.authorization.IAuthorizerProviderPlugin

class UnderwritingQuoteRetrievalHandler extends UnderwritingQuoteHandler {

  var _paymentPlanPlugin: IUnderwritingPaymentPlanPlugin

  construct() {
  }

  @InjectableNode
  @Param("basePlugin", "Plugin used to manage base quote information")
  @Param("lobPlugin", "Plugin used to manage lob specific information")
  @Param("lobCoveragesUpdatePlugin", "Plugin used to update coverage information")
  @Param("sessionPlugin", "Session management plugin")
  @Param("quotingPlugin", "Quoting process plugin implementation")
  @Param("bindingPlugin", "Binding process plugin")
  @Param("quoteMailingPlugin", "Plugin used to mail the quote")
  @Param("lobMetadataPlugin", "Plugin used to extend metadata generation")
  @Param("authorizer", "Authorizer added to check user can retrieve submission")
  @Param("accContactPlugin", "Plugin used for creating and updating the account holder")
  @Param("quoteDocumentPlugin", "Plugin used for creating and storing Policy Quote documents")
  @Param("industryCodePlugin", "Plugin used for retrieving industry codes")
  @Param("validationPlugin", "Plugin used to perform entity validation")
  @Param("aUWIssuePlugin", "Plugin used to perform serialization of UW issues")
  construct(
      basePlugin: IBaseSubmissionPlugin,
      underwritingBasePlugin: IUnderwritingBaseSubmissionPlugin,
      lobPlugin: IQuoteLobDataPlugin<LobDTO>,
      lobCoveragesUpdatePlugin: ILobCoveragesPlugin<LobCoveragesDTO>,
      sessionPlugin: ISessionPlugin,
      quotingPlugin: ISubmissionQuotePlugin,
      bindingPlugin: IBindingPlugin,
      quoteMailingPlugin: IQuoteMailingPlugin,
      lobMetadataPlugin: ILobMetadataPlugin,
      authorizer: IAuthorizerProviderPlugin,
      accContactPlugin: IAccountContactPlugin,
      underwritingBindingPlugin: IUnderwritingBindingPlugin,
      issuePlugin: IIssuePlugin,
      policyPeriodRetrievalPlugin: IPolicyPeriodRetrievalPlugin,
      submissionRetrievalPlugin: ISubmissionRetrievalPlugin,
      policyPeriodPlugin: IUnderwritingPolicyPeriodPlugin,
      quoteDocumentPlugin: IQuoteDocumentPlugin,
      industryCodePlugin: IIndustryCodePlugin,
      paymentPlanPlugin: IUnderwritingPaymentPlanPlugin,
      validationPlugin : IEdgeValidationRulesPlugin,
      aUWIssuePlugin : IUWIssuePlugin
  ) {
    super(basePlugin, underwritingBasePlugin, lobPlugin, lobCoveragesUpdatePlugin, sessionPlugin, quotingPlugin, bindingPlugin, quoteMailingPlugin,
        lobMetadataPlugin, authorizer, accContactPlugin, underwritingBindingPlugin, issuePlugin, policyPeriodRetrievalPlugin, submissionRetrievalPlugin,
        policyPeriodPlugin, quoteDocumentPlugin, industryCodePlugin, validationPlugin, aUWIssuePlugin)
    _paymentPlanPlugin = paymentPlanPlugin
  }

  @JsonRpcUnauthenticatedMethod
  @ApidocMethodDescription("Retrieves a submission.")
  @ApidocAvailableSince("10.0 UM-PC Integration")
  public function retrieve(qrd: QuoteRetrievalDTO): QuoteDataDTO {
    var res = super.retrieve(qrd)
    var submission = getSubmissionByJob(res.QuoteID)
    var boundPeriod = submission.ResultingBoundPeriod
    if (boundPeriod != null) {
      _paymentPlanPlugin.updatePaymentPlanWithTaxesAndFees(res.BindData.PaymentPlans.first(), boundPeriod)
    }
    return res
  }

  /**
   * Retrieves the payment plans for the current submission
   *
   * @param quoteID   quoteID of submission that needs to be found
   * @param sessionId current session id
   * @return a list of existing payment plans for the given Submission
   */
  @JsonRpcUnauthenticatedMethod
  @ApidocMethodDescription("Retrieves the payment plans for the given submission.")
  @ApidocAvailableSince("10.0 UM-PC Integration")
  public function retrievePaymentPlans(quoteID: String, sessionId: String): PaymentPlanDTO[] {
    var paymentPlans = super.retrievePaymentPlans(quoteID, sessionId)
    var submission = getSubmissionByJob(quoteID)
    var selectedVersion = submission.SelectedVersion
    if (selectedVersion != null) {
      paymentPlans.each(\plan ->
          _paymentPlanPlugin.updatePaymentPlanWithTaxesAndFees(plan, selectedVersion)
      )
    }
    return paymentPlans
  }

}
package edge.capabilities.quote.submission

uses edge.capabilities.gateway.job.submission.ISubmissionRetrievalPlugin
uses edge.capabilities.gateway.policy.IPolicyPeriodRetrievalPlugin
uses edge.capabilities.gateway.policy.IUnderwritingPolicyPeriodPlugin
uses edge.capabilities.industrycode.IIndustryCodePlugin
uses edge.capabilities.policycommon.accountcontact.IAccountContactPlugin
uses edge.capabilities.policycommon.validation.IEdgeValidationRulesPlugin
uses edge.capabilities.policycommon.validation.IUWIssuePlugin
uses edge.capabilities.policyjob.binding.IBindingPlugin
uses edge.capabilities.policyjob.binding.IUnderwritingBindingPlugin
uses edge.capabilities.policyjob.binding.IUnderwritingPaymentPlanPlugin
uses edge.capabilities.policyjob.binding.dto.PaymentPlanDTO
uses edge.capabilities.policyjob.lob.ILobCoveragesPlugin
uses edge.capabilities.policyjob.lob.dto.LobCoveragesDTO
uses edge.capabilities.quote.helper.session.ISessionPlugin
uses edge.capabilities.quote.mailing.IQuoteMailingPlugin
uses edge.capabilities.quote.submission.base.IBaseSubmissionPlugin
uses edge.capabilities.quote.submission.base.IUnderwritingBaseSubmissionPlugin
uses edge.capabilities.quote.submission.dto.QuoteDataDTO
uses edge.capabilities.quote.submission.dto.QuoteRetrievalDTO
uses edge.capabilities.quote.submission.issuing.IIssuePlugin
uses edge.capabilities.quote.submission.lob.ILobMetadataPlugin
uses edge.capabilities.quote.submission.lob.IQuoteLobDataPlugin
uses edge.capabilities.quote.submission.lob.LobDTO
uses edge.capabilities.quote.submission.quotedocument.IQuoteDocumentPlugin
uses edge.capabilities.quote.submission.quoting.ISubmissionQuotePlugin
uses edge.di.annotations.InjectableNode
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.annotation.JsonRpcUnauthenticatedMethod
uses edge.security.authorization.IAuthorizerProviderPlugin
uses edge.security.EffectiveUserProvider

class UnderwritingQuoteRetrievalHandler extends UnderwritingQuoteHandler {

  var _paymentPlanPlugin: IUnderwritingPaymentPlanPlugin

  construct() {
  }

  @InjectableNode
  @Param("basePlugin", "Plugin used to manage base quote information")
  @Param("lobPlugin", "Plugin used to manage lob specific information")
  @Param("lobCoveragesUpdatePlugin", "Plugin used to update coverage information")
  @Param("sessionPlugin", "Session management plugin")
  @Param("quotingPlugin", "Quoting process plugin implementation")
  @Param("bindingPlugin", "Binding process plugin")
  @Param("quoteMailingPlugin", "Plugin used to mail the quote")
  @Param("lobMetadataPlugin", "Plugin used to extend metadata generation")
  @Param("authorizer", "Authorizer added to check user can retrieve submission")
  @Param("accContactPlugin", "Plugin used for creating and updating the account holder")
  @Param("quoteDocumentPlugin", "Plugin used for creating and storing Policy Quote documents")
  @Param("industryCodePlugin", "Plugin used for retrieving industry codes")
  @Param("validationPlugin", "Plugin used to perform entity validation")
  @Param("aUWIssuePlugin", "Plugin used to perform serialization of UW issues")
  @Param("aUserProvider", "Provider for the effective user, used to verify account ownership")
  construct(
      basePlugin: IBaseSubmissionPlugin,
      underwritingBasePlugin: IUnderwritingBaseSubmissionPlugin,
      lobPlugin: IQuoteLobDataPlugin<LobDTO>,
      lobCoveragesUpdatePlugin: ILobCoveragesPlugin<LobCoveragesDTO>,
      sessionPlugin: ISessionPlugin,
      quotingPlugin: ISubmissionQuotePlugin,
      bindingPlugin: IBindingPlugin,
      quoteMailingPlugin: IQuoteMailingPlugin,
      lobMetadataPlugin: ILobMetadataPlugin,
      authorizer: IAuthorizerProviderPlugin,
      accContactPlugin: IAccountContactPlugin,
      underwritingBindingPlugin: IUnderwritingBindingPlugin,
      issuePlugin: IIssuePlugin,
      policyPeriodRetrievalPlugin: IPolicyPeriodRetrievalPlugin,
      submissionRetrievalPlugin: ISubmissionRetrievalPlugin,
      policyPeriodPlugin: IUnderwritingPolicyPeriodPlugin,
      quoteDocumentPlugin: IQuoteDocumentPlugin,
      industryCodePlugin: IIndustryCodePlugin,
      paymentPlanPlugin: IUnderwritingPaymentPlanPlugin,
      validationPlugin : IEdgeValidationRulesPlugin,
      aUWIssuePlugin : IUWIssuePlugin,
      aUserProvider: EffectiveUserProvider
  ) {
    super(basePlugin, underwritingBasePlugin, lobPlugin, lobCoveragesUpdatePlugin, sessionPlugin, quotingPlugin, bindingPlugin, quoteMailingPlugin,
        lobMetadataPlugin, authorizer, accContactPlugin, underwritingBindingPlugin, issuePlugin, policyPeriodRetrievalPlugin, submissionRetrievalPlugin,
        policyPeriodPlugin, quoteDocumentPlugin, industryCodePlugin, validationPlugin, aUWIssuePlugin, aUserProvider)
    _paymentPlanPlugin = paymentPlanPlugin
  }

  @JsonRpcUnauthenticatedMethod
  @ApidocMethodDescription("Retrieves a submission.")
  @ApidocAvailableSince("10.0 UM-PC Integration")
  public function retrieve(qrd: QuoteRetrievalDTO): QuoteDataDTO {
    var res = super.retrieve(qrd)
    var submission = getSubmissionByJob(res.QuoteID)
    var boundPeriod = submission.ResultingBoundPeriod
    if (boundPeriod != null) {
      _paymentPlanPlugin.updatePaymentPlanWithTaxesAndFees(res.BindData.PaymentPlans.first(), boundPeriod)
    }
    return res
  }

  /**
   * Retrieves the payment plans for the current submission
   *
   * @param quoteID   quoteID of submission that needs to be found
   * @param sessionId current session id
   * @return a list of existing payment plans for the given Submission
   */
  @JsonRpcUnauthenticatedMethod
  @ApidocMethodDescription("Retrieves the payment plans for the given submission.")
  @ApidocAvailableSince("10.0 UM-PC Integration")
  public function retrievePaymentPlans(quoteID: String, sessionId: String): PaymentPlanDTO[] {
    var paymentPlans = super.retrievePaymentPlans(quoteID, sessionId)
    var submission = getSubmissionByJob(quoteID)
    var selectedVersion = submission.SelectedVersion
    if (selectedVersion != null) {
      paymentPlans.each(\plan ->
          _paymentPlanPlugin.updatePaymentPlanWithTaxesAndFees(plan, selectedVersion)
      )
    }
    return paymentPlans
  }

}

UnderwritingQuoteHandler changes

The following changes need to be made to /gsrc/edge/capabilities/quote/submission/UnderwritingQuoteHandler.gs‎:

Original file content:
package edge.capabilities.quote.submission

uses edge.PlatformSupport.Bundle
uses edge.aspects.validation.annotations.Context
uses edge.capabilities.gateway.job.submission.ISubmissionRetrievalPlugin
uses edge.capabilities.gateway.policy.IPolicyPeriodRetrievalPlugin
uses edge.capabilities.gateway.policy.IUnderwritingPolicyPeriodPlugin
uses edge.capabilities.industrycode.IIndustryCodePlugin
uses edge.capabilities.industrycode.dto.IndustryCodeDTO
uses edge.capabilities.industrycode.dto.IndustryCodeSearchCriteriaDTO
uses edge.capabilities.policycommon.accountcontact.IAccountContactPlugin
uses edge.capabilities.policycommon.validation.IEdgeValidationRulesPlugin
uses edge.capabilities.policycommon.validation.IUWIssuePlugin
uses edge.capabilities.policyjob.binding.IBindingPlugin
uses edge.capabilities.policyjob.binding.IUnderwritingBindingPlugin
uses edge.capabilities.policyjob.lob.ILobCoveragesPlugin
uses edge.capabilities.policyjob.lob.dto.LobCoveragesDTO
uses edge.capabilities.policyjob.quoting.exception.UnderwritingException
uses edge.capabilities.policyjob.quoting.util.QuoteUtil
uses edge.capabilities.quote.helper.session.ISessionPlugin
uses edge.capabilities.quote.mailing.IQuoteMailingPlugin
uses edge.capabilities.quote.submission.base.IBaseSubmissionPlugin
uses edge.capabilities.quote.submission.base.IUnderwritingBaseSubmissionPlugin
uses edge.capabilities.quote.submission.dto.IssuanceDTO
uses edge.capabilities.quote.submission.dto.QuoteDataDTO
uses edge.capabilities.quote.submission.dto.QuoteDocumentDTO
uses edge.capabilities.quote.submission.dto.QuoteOnlyRequestDTO
uses edge.capabilities.quote.submission.dto.UnderwritingQuoteDataDTO
uses edge.capabilities.quote.submission.dto.UpdateDraftSubmissionRequestDTO
uses edge.capabilities.quote.submission.dto.UpdateDraftSubmissionResponseDTO
uses edge.capabilities.quote.submission.issuing.IIssuePlugin
uses edge.capabilities.quote.submission.lob.ILobMetadataPlugin
uses edge.capabilities.quote.submission.lob.IQuoteLobDataPlugin
uses edge.capabilities.quote.submission.lob.LobDTO
uses edge.capabilities.quote.submission.quotedocument.IQuoteDocumentPlugin
uses edge.capabilities.quote.submission.quoting.ISubmissionQuotePlugin
uses edge.capabilities.quote.submission.quoting.exception.BlockQuoteUnderwritingException
uses edge.capabilities.quote.submission.quoting.exception.EntityValidationException
uses edge.di.annotations.InjectableNode
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.el.Expr
uses edge.exception.EntityNotFoundException
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.jsonrpc.annotation.JsonRpcRunAsInternalGWUser
uses edge.jsonrpc.annotation.JsonRpcUnauthenticatedMethod
uses edge.security.authorization.IAuthorizerProviderPlugin
uses java.lang.IllegalArgumentException
uses java.lang.IllegalStateException
uses gw.api.privacy.EncryptionMaskExpressions

class UnderwritingQuoteHandler extends QuoteHandler {

  var _underwritingBindingPlugin: IUnderwritingBindingPlugin
  var _issuePlugin: IIssuePlugin
  var _policyPeriodRetrievalPlugin: IPolicyPeriodRetrievalPlugin
  var _underwritingBasePlugin: IUnderwritingBaseSubmissionPlugin
  var _submissionRetrievalPlugin: ISubmissionRetrievalPlugin
  var _policyPeriodPlugin: IUnderwritingPolicyPeriodPlugin
  var _quoteDocumentPlugin: IQuoteDocumentPlugin
  var _industryCodePlugin: IIndustryCodePlugin

  construct() {
  }

  @InjectableNode
  @Param("basePlugin", "Plugin used to manage base quote information")
  @Param("lobPlugin", "Plugin used to manage lob specific information")
  @Param("lobCoveragesUpdatePlugin", "Plugin used to update coverage information")
  @Param("sessionPlugin", "Session management plugin")
  @Param("quotingPlugin", "Quoting process plugin implementation")
  @Param("bindingPlugin", "Binding process plugin")
  @Param("quoteMailingPlugin", "Plugin used to mail the quote")
  @Param("lobMetadataPlugin", "Plugin used to extend metadata generation")
  @Param("authorizer", "Authorizer added to check user can retrieve submission")
  @Param("accContactPlugin", "Plugin used for creating and updating the account holder")
  @Param("quoteDocumentPlugin", "Plugin used for creating and storing Policy Quote documents")
  @Param("industryCodePlugin", "Plugin used for retrieving industry codes")
  @Param("validationPlugin", "Plugin used to perform entity validation")
  @Param("aUWIssuePlugin", "Plugin used to perform serialization of UW issues")
  construct(
      basePlugin: IBaseSubmissionPlugin,
      underwritingBasePlugin: IUnderwritingBaseSubmissionPlugin,
      lobPlugin: IQuoteLobDataPlugin<LobDTO>,
      lobCoveragesUpdatePlugin: ILobCoveragesPlugin<LobCoveragesDTO>,
      sessionPlugin: ISessionPlugin,
      quotingPlugin: ISubmissionQuotePlugin,
      bindingPlugin: IBindingPlugin,
      quoteMailingPlugin: IQuoteMailingPlugin,
      lobMetadataPlugin: ILobMetadataPlugin,
      authorizer: IAuthorizerProviderPlugin,
      accContactPlugin: IAccountContactPlugin,
      underwritingBindingPlugin: IUnderwritingBindingPlugin,
      issuePlugin: IIssuePlugin,
      policyPeriodRetrievalPlugin: IPolicyPeriodRetrievalPlugin,
      submissionRetrievalPlugin: ISubmissionRetrievalPlugin,
      policyPeriodPlugin: IUnderwritingPolicyPeriodPlugin,
      quoteDocumentPlugin: IQuoteDocumentPlugin,
      industryCodePlugin: IIndustryCodePlugin,
      validationPlugin : IEdgeValidationRulesPlugin,
      aUWIssuePlugin : IUWIssuePlugin) {
    super(
        basePlugin,
        lobPlugin,
        lobCoveragesUpdatePlugin,
        sessionPlugin,
        quotingPlugin,
        bindingPlugin,
        quoteMailingPlugin,
        lobMetadataPlugin,
        authorizer,
        accContactPlugin,
        validationPlugin,
        aUWIssuePlugin)
    _underwritingBindingPlugin = underwritingBindingPlugin
    _issuePlugin = issuePlugin
    _policyPeriodRetrievalPlugin = policyPeriodRetrievalPlugin
    _underwritingBasePlugin = underwritingBasePlugin
    _submissionRetrievalPlugin = submissionRetrievalPlugin
    _policyPeriodPlugin = policyPeriodPlugin
    _quoteDocumentPlugin = quoteDocumentPlugin
    _industryCodePlugin = industryCodePlugin
  }
Updated file content:
package edge.capabilities.quote.submission

uses edge.PlatformSupport.Bundle
uses edge.aspects.validation.annotations.Context
uses edge.capabilities.gateway.job.submission.ISubmissionRetrievalPlugin
uses edge.capabilities.gateway.policy.IPolicyPeriodRetrievalPlugin
uses edge.capabilities.gateway.policy.IUnderwritingPolicyPeriodPlugin
uses edge.capabilities.industrycode.IIndustryCodePlugin
uses edge.capabilities.industrycode.dto.IndustryCodeDTO
uses edge.capabilities.industrycode.dto.IndustryCodeSearchCriteriaDTO
uses edge.capabilities.policycommon.accountcontact.IAccountContactPlugin
uses edge.capabilities.policycommon.validation.IEdgeValidationRulesPlugin
uses edge.capabilities.policycommon.validation.IUWIssuePlugin
uses edge.capabilities.policyjob.binding.IBindingPlugin
uses edge.capabilities.policyjob.binding.IUnderwritingBindingPlugin
uses edge.capabilities.policyjob.lob.ILobCoveragesPlugin
uses edge.capabilities.policyjob.lob.dto.LobCoveragesDTO
uses edge.capabilities.policyjob.quoting.exception.UnderwritingException
uses edge.capabilities.policyjob.quoting.util.QuoteUtil
uses edge.capabilities.quote.helper.session.ISessionPlugin
uses edge.capabilities.quote.mailing.IQuoteMailingPlugin
uses edge.capabilities.quote.submission.base.IBaseSubmissionPlugin
uses edge.capabilities.quote.submission.base.IUnderwritingBaseSubmissionPlugin
uses edge.capabilities.quote.submission.dto.IssuanceDTO
uses edge.capabilities.quote.submission.dto.QuoteDataDTO
uses edge.capabilities.quote.submission.dto.QuoteDocumentDTO
uses edge.capabilities.quote.submission.dto.QuoteOnlyRequestDTO
uses edge.capabilities.quote.submission.dto.UnderwritingQuoteDataDTO
uses edge.capabilities.quote.submission.dto.UpdateDraftSubmissionRequestDTO
uses edge.capabilities.quote.submission.dto.UpdateDraftSubmissionResponseDTO
uses edge.capabilities.quote.submission.issuing.IIssuePlugin
uses edge.capabilities.quote.submission.lob.ILobMetadataPlugin
uses edge.capabilities.quote.submission.lob.IQuoteLobDataPlugin
uses edge.capabilities.quote.submission.lob.LobDTO
uses edge.capabilities.quote.submission.quotedocument.IQuoteDocumentPlugin
uses edge.capabilities.quote.submission.quoting.ISubmissionQuotePlugin
uses edge.capabilities.quote.submission.quoting.exception.BlockQuoteUnderwritingException
uses edge.capabilities.quote.submission.quoting.exception.EntityValidationException
uses edge.di.annotations.InjectableNode
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.el.Expr
uses edge.exception.EntityNotFoundException
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.jsonrpc.annotation.JsonRpcRunAsInternalGWUser
uses edge.jsonrpc.annotation.JsonRpcUnauthenticatedMethod
uses edge.security.authorization.IAuthorizerProviderPlugin
uses edge.security.EffectiveUserProvider
uses java.lang.IllegalArgumentException
uses java.lang.IllegalStateException
uses gw.api.privacy.EncryptionMaskExpressions

class UnderwritingQuoteHandler extends QuoteHandler {

  var _underwritingBindingPlugin: IUnderwritingBindingPlugin
  var _issuePlugin: IIssuePlugin
  var _policyPeriodRetrievalPlugin: IPolicyPeriodRetrievalPlugin
  var _underwritingBasePlugin: IUnderwritingBaseSubmissionPlugin
  var _submissionRetrievalPlugin: ISubmissionRetrievalPlugin
  var _policyPeriodPlugin: IUnderwritingPolicyPeriodPlugin
  var _quoteDocumentPlugin: IQuoteDocumentPlugin
  var _industryCodePlugin: IIndustryCodePlugin

  construct() {
  }

  @InjectableNode
  @Param("basePlugin", "Plugin used to manage base quote information")
  @Param("lobPlugin", "Plugin used to manage lob specific information")
  @Param("lobCoveragesUpdatePlugin", "Plugin used to update coverage information")
  @Param("sessionPlugin", "Session management plugin")
  @Param("quotingPlugin", "Quoting process plugin implementation")
  @Param("bindingPlugin", "Binding process plugin")
  @Param("quoteMailingPlugin", "Plugin used to mail the quote")
  @Param("lobMetadataPlugin", "Plugin used to extend metadata generation")
  @Param("authorizer", "Authorizer added to check user can retrieve submission")
  @Param("accContactPlugin", "Plugin used for creating and updating the account holder")
  @Param("quoteDocumentPlugin", "Plugin used for creating and storing Policy Quote documents")
  @Param("industryCodePlugin", "Plugin used for retrieving industry codes")
  @Param("validationPlugin", "Plugin used to perform entity validation")
  @Param("aUWIssuePlugin", "Plugin used to perform serialization of UW issues")
  @Param("aUserProvider", "Provider for the effective user, used to verify account ownership")
  construct(
      basePlugin: IBaseSubmissionPlugin,
      underwritingBasePlugin: IUnderwritingBaseSubmissionPlugin,
      lobPlugin: IQuoteLobDataPlugin<LobDTO>,
      lobCoveragesUpdatePlugin: ILobCoveragesPlugin<LobCoveragesDTO>,
      sessionPlugin: ISessionPlugin,
      quotingPlugin: ISubmissionQuotePlugin,
      bindingPlugin: IBindingPlugin,
      quoteMailingPlugin: IQuoteMailingPlugin,
      lobMetadataPlugin: ILobMetadataPlugin,
      authorizer: IAuthorizerProviderPlugin,
      accContactPlugin: IAccountContactPlugin,
      underwritingBindingPlugin: IUnderwritingBindingPlugin,
      issuePlugin: IIssuePlugin,
      policyPeriodRetrievalPlugin: IPolicyPeriodRetrievalPlugin,
      submissionRetrievalPlugin: ISubmissionRetrievalPlugin,
      policyPeriodPlugin: IUnderwritingPolicyPeriodPlugin,
      quoteDocumentPlugin: IQuoteDocumentPlugin,
      industryCodePlugin: IIndustryCodePlugin,
      validationPlugin : IEdgeValidationRulesPlugin,
      aUWIssuePlugin : IUWIssuePlugin,
      aUserProvider: EffectiveUserProvider) {
    super(
        basePlugin,
        lobPlugin,
        lobCoveragesUpdatePlugin,
        sessionPlugin,
        quotingPlugin,
        bindingPlugin,
        quoteMailingPlugin,
        lobMetadataPlugin,
        authorizer,
        accContactPlugin,
        validationPlugin,
        aUWIssuePlugin,
        aUserProvider)
    _underwritingBindingPlugin = underwritingBindingPlugin
    _issuePlugin = issuePlugin
    _policyPeriodRetrievalPlugin = policyPeriodRetrievalPlugin
    _underwritingBasePlugin = underwritingBasePlugin
    _submissionRetrievalPlugin = submissionRetrievalPlugin
    _policyPeriodPlugin = policyPeriodPlugin
    _quoteDocumentPlugin = quoteDocumentPlugin
    _industryCodePlugin = industryCodePlugin
  }

EdgeAuthenticationSourceCreator plugin changes

The following changes need to be made to /gsrc/edge/oauth/authplugin/EdgeAuthenticationSourceCreatorPlugin.gs:

Original file content:
package edge.oauth.authplugin

uses gw.auth.gwhub.AuthSourceCreatorPlugin
uses gw.plugin.security.AuthenticationSource
uses jakarta.servlet.http.HttpServletRequest
uses gw.plugin.security.UserNamePasswordAuthenticationSource
uses java.nio.charset.Charset
uses gw.util.Base64Util
uses gw.plugin.security.InvalidAuthenticationSourceData
uses com.nimbusds.jwt.JWTClaimsSet
uses java.lang.Exception
uses edge.servlet.jsonrpc.JsonRpcUnauthenticatedServlet
uses edge.oauth.authplugin.OAuthAuthenticationSource

class EdgeAuthenticationSourceCreatorPlugin extends AuthSourceCreatorPlugin {
  private static final var OAUTH_ATTR_NAME = "OAuthSource"
  private static final var GW_USER_CONTEXT = "GWUserContext"

  override function createSourceFromHTTPRequest(req: HttpServletRequest): AuthenticationSource {
    if (req.getAttribute(JsonRpcUnauthenticatedServlet.UNAUTHENTICATED_USER) != null) {
      return new AnonymousAuthenticationSource()
    }

    var source : AuthenticationSource
    source = getOAuthAuthenticationSource(req)
    if (source == null) {
      source = this.getUserNamePasswordAuthenticationSource(req)
    }
    if (source == null) {
      source = super.createSourceFromHTTPRequest(req)
    }

    return source
  }


  private function getOAuthAuthenticationSource(req: HttpServletRequest): OAuthAuthenticationSource {
    var authHeader = req.getHeader("Authorization")
    var accessToken : String
    if (authHeader != null && authHeader.indexOf("Bearer") != -1) {
      accessToken = authHeader.substring("Bearer".length())?.trim()
    }
    if (req.ParameterNames.toList().contains("access_token")) {
      accessToken = req.getParameter("access_token")
    }
    if(accessToken?.NotBlank) {
      var jwtPlugin = new JwtVerificationPlugin()
      var claimsSet: JWTClaimsSet = null
      try {
        claimsSet = jwtPlugin.verifyToken(accessToken)
      } catch (e: Exception) {
        throw new InvalidAuthenticationSourceData(e)
      }

      var userContextBase64 = req.getHeader(GW_USER_CONTEXT as String)
      var userContext : String
      var oAuthSource : OAuthAuthenticationSource

      if (userContextBase64 != null) {
        userContext = new String(Base64Util.decode(userContextBase64), Charset.forName("UTF-8"))
      }

      oAuthSource = new OAuthAuthenticationSource(claimsSet, userContext)
      req.setAttribute(OAUTH_ATTR_NAME, oAuthSource)
      return oAuthSource
    }
    return null
  }
...
Updated file content:
package edge.oauth.authplugin

uses gw.auth.gwhub.AuthSourceCreatorPlugin
uses gw.plugin.security.AuthenticationSource
uses jakarta.servlet.http.HttpServletRequest
uses gw.plugin.security.UserNamePasswordAuthenticationSource
uses java.nio.charset.Charset
uses gw.util.Base64Util
uses gw.plugin.security.InvalidAuthenticationSourceData
uses com.nimbusds.jwt.JWTClaimsSet
uses java.lang.Exception
uses edge.servlet.jsonrpc.JsonRpcUnauthenticatedServlet
uses edge.oauth.authplugin.OAuthAuthenticationSource

class EdgeAuthenticationSourceCreatorPlugin extends AuthSourceCreatorPlugin {
  private static final var OAUTH_ATTR_NAME = "OAuthSource"
  private static final var GW_USER_CONTEXT = "GWUserContext"

  override function createSourceFromHTTPRequest(req: HttpServletRequest): AuthenticationSource {
    if (req.getAttribute(JsonRpcUnauthenticatedServlet.UNAUTHENTICATED_USER) != null) {
      // Even on the unauthenticated servlet path, attempt to extract and store the JWT
      // as an OAuthAuthenticationSource attribute. This allows the EffectiveUserProvider
      // to be populated with the caller's JWT identity (including ACCOUNT authority claims)
      // for ownership checks, without changing the PC user resolution (still anonymous).
      getOAuthAuthenticationSource(req)
      return new AnonymousAuthenticationSource()
    }

    var source : AuthenticationSource
    source = getOAuthAuthenticationSource(req)
    if (source == null) {
      source = this.getUserNamePasswordAuthenticationSource(req)
    }
    if (source == null) {
      source = super.createSourceFromHTTPRequest(req)
    }

    return source
  }


  private function getOAuthAuthenticationSource(req: HttpServletRequest): OAuthAuthenticationSource {
    var authHeader = req.getHeader("Authorization")
    var accessToken : String
    if (authHeader != null && authHeader.indexOf("Bearer") != -1) {
      accessToken = authHeader.substring("Bearer".length())?.trim()
    }
    if (req.ParameterNames.toList().contains("access_token")) {
      accessToken = req.getParameter("access_token")
    }
    if(accessToken?.NotBlank) {
      var jwtPlugin = new JwtVerificationPlugin()
      var claimsSet: JWTClaimsSet = null
      try {
        claimsSet = jwtPlugin.verifyToken(accessToken)
      } catch (e: Exception) {
        throw new InvalidAuthenticationSourceData(e)
      }

      var userContextBase64 = req.getHeader(GW_USER_CONTEXT as String)
      var userContext : String

      if (userContextBase64 != null) {
        userContext = new String(Base64Util.decode(userContextBase64), Charset.forName("UTF-8"))
      }

      var oAuthSource = new OAuthAuthenticationSource(claimsSet, userContext)
      req.setAttribute(OAUTH_ATTR_NAME, oAuthSource)
      return oAuthSource
    }
    return null
  }
...

DefaultHttpRequestUserIdentity plugin changes

The following changes need to be made to /gsrc/edge/servlet/security/DefaultHttpRequestUserIdentityPlugin.gs:

Original file content:
class DefaultHttpRequestUserIdentityPlugin implements IHttpRequestUserIdentityPlugin {

  private static final var OAUTH_ATTR_NAME = "OAuthSource"
  private static final var GW_USER_CONTEXT = "GWUserContext"


  @ForAllGwNodes
  construct() {
  }

  override function getEffectiveUserFromRequest(req : HttpServletRequest) : EffectiveUser {
    var effectiveUser : EffectiveUser
    var hasOAuthSource = req.getAttribute(OAUTH_ATTR_NAME) != null

    if (hasOAuthSource) {
      return getEffectiveUserFromOAuthSource(req)
    }

    if (req.getAttribute(JsonRpcUnauthenticatedServlet.UNAUTHENTICATED_USER) != null) {
      //anonymous access
      effectiveUser = new EffectiveUser(null, null, null, null)
    } else {
      var currentUser = User.util.CurrentUser
      //Internal user logging in with username and password
      effectiveUser = new EffectiveUser(currentUser.Credential.UserName, null, currentUser, null)
    }
    return effectiveUser
  }

  private function getEffectiveUserFromOAuthSource(req : HttpServletRequest) : EffectiveUser {
    final var currentUser = User.util.CurrentUser
    final var accessToken = getAccessToken(req)
    final var oAuthSource = req.getAttribute(OAUTH_ATTR_NAME) as OAuthAuthenticationSource
    final var userContext = req.getHeader(GW_USER_CONTEXT)

    var userId = getUserId(oAuthSource.Claims)

    if (oAuthSource.IsInternalUser) {
      //Internal user logging in with jwt token issued by xcenter
      return new EffectiveUser(oAuthSource.Username, null, currentUser, userId, accessToken)
    }

    if (oAuthSource.IsServiceUser) {
      var user = oAuthSource.serviceUser
      if (oAuthSource.IsInternalUserForThisApplication) {

        if (user == null) {
          throw new IllegalArgumentException("Access token user identifier not found in request header")
        }
      }
      return new EffectiveUser(user, null, currentUser, userId, accessToken, userContext)
    }

    if (oAuthSource.HasServiceUserAuthorities) {
      if (oAuthSource.serviceUserAuthorities == null) {
        throw new IllegalArgumentException("Access token user authorities not found in request header")
      }

      var grantedAuthorities = getGrantedAuthorities(oAuthSource.Claims, oAuthSource.serviceUserAuthorities)
      return new EffectiveUser(oAuthSource.Username, grantedAuthorities, null, userId, accessToken, userContext)
    }
Updated file content:
class DefaultHttpRequestUserIdentityPlugin implements IHttpRequestUserIdentityPlugin {

  private static final var OAUTH_ATTR_NAME = "OAuthSource"
  private static final var GW_USER_CONTEXT = "GWUserContext"


  @ForAllGwNodes
  construct() {
  }

  override function getEffectiveUserFromRequest(req : HttpServletRequest) : EffectiveUser {
    var effectiveUser : EffectiveUser
    var hasOAuthSource = req.getAttribute(OAUTH_ATTR_NAME) != null

    if (hasOAuthSource) {
      return getEffectiveUserFromOAuthSource(req)
    }

    if (req.getAttribute(JsonRpcUnauthenticatedServlet.UNAUTHENTICATED_USER) != null) {
      //anonymous access
      effectiveUser = new EffectiveUser(null, null, null, null)
    } else {
      var currentUser = User.util.CurrentUser
      //Internal user logging in with username and password (no prior JWT on this session)
      effectiveUser = new EffectiveUser(currentUser.Credential.UserName, null, currentUser, null)
    }
    return effectiveUser
  }

  private function getEffectiveUserFromOAuthSource(req : HttpServletRequest) : EffectiveUser {
    final var currentUser = User.util.CurrentUser
    final var accessToken = getAccessToken(req)
    final var oAuthSource = req.getAttribute(OAUTH_ATTR_NAME) as OAuthAuthenticationSource
    final var userContext = req.getHeader(GW_USER_CONTEXT)

    var userId = getUserId(oAuthSource.Claims)

    if (oAuthSource.IsInternalUser) {
      //Internal user logging in with jwt token issued by xcenter.
      // Also parse grantedAuthorities from the JWT when present (e.g. portal users
      // who have a pcUserName claim AND account-scoped authorities in the same token).
      var internalGrantedAuthorities = oAuthSource.Claims.Claims.get("grantedAuthorities") != null
          ? getGrantedAuthorities(oAuthSource.Claims, null)
          : null
      return new EffectiveUser(oAuthSource.Username, internalGrantedAuthorities, currentUser, userId, accessToken)
    }

    if (oAuthSource.IsServiceUser) {
      var user = oAuthSource.serviceUser
      if (oAuthSource.IsInternalUserForThisApplication) {

        if (user == null) {
          throw new IllegalArgumentException("Access token user identifier not found in request header")
        }
      }
      // Parse grantedAuthorities from the JWT when present — service/portal users
      // may have account-scoped authorities alongside the service-user scope.
      var serviceGrantedAuthorities = oAuthSource.Claims.Claims.get("grantedAuthorities") != null
          ? getGrantedAuthorities(oAuthSource.Claims, null)
          : null
      return new EffectiveUser(user, serviceGrantedAuthorities, currentUser, userId, accessToken, userContext)
    }

    if (oAuthSource.HasServiceUserAuthorities) {
      if (oAuthSource.serviceUserAuthorities == null) {
        throw new IllegalArgumentException("Access token user authorities not found in request header")
      }

      var grantedAuthorities = getGrantedAuthorities(oAuthSource.Claims, oAuthSource.serviceUserAuthorities)
      return new EffectiveUser(oAuthSource.Username, grantedAuthorities, null, userId, accessToken, userContext)
    }

HOWizard changes

The following changes need to be made to applications/common/capabilities-react/gw-capability-quoteandbind-ho-react/HOWizard.jsx:

Original file content:
function HOWizard(props) {
    const {
        showConfirm,
        showAlert
    } = useModal();

    const { steps, title } = wizardConfig;
    const [initialSubmission, setInitialSubmission] = useState(null);
    const [isLoading, setIsLoading] = useState(true);
    const [shouldSkipValidSteps, setShouldSkipValidSteps] = useState(false);
    const viewModelService = useContext(ViewModelServiceContext);
    const { location, history } = props;
    useEffect(() => {
        const viewModelContext = {
            AccountEmailRequired: true,
            AccountDOBRequired: true
        };

        let submissionPromise;

        if (!viewModelService) {
            if (_.isEmpty(location.search)) {
                history.push('/');
            }

            return;
        }

        if (_.has(location, 'state.address')) {
            const { address } = location.state;
            submissionPromise = createSubmissionOnPartialAddress(address);
        } else if (_.has(location, 'state.submission')) {
            const { submission } = location.state;
            setShouldSkipValidSteps(true);
            submissionPromise = Promise.resolve(submission);
        } else if (_.has(location, 'search')) {
            const parsedParms = queryString.parse(location.search);
            submissionPromise = LoadSaveService.retrieveAccountSubmission(parsedParms);
        } else {
            return;
        }

        submissionPromise.then((response) => {
            const submissionVM = viewModelService.create(
                removeMockData(response),
                'pc',
                'edge.capabilities.quote.submission.dto.QuoteDataDTO',
                viewModelContext
            );

            setInitialSubmission(submissionVM);
            setIsLoading(false);
        });
        // only execute this once per component lifecycle
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [viewModelService]);
...
Updated file content:
function HOWizard(props) {
    const {
        showConfirm,
        showAlert
    } = useModal();

    const { steps, title } = wizardConfig;
    const [initialSubmission, setInitialSubmission] = useState(null);
    const [isLoading, setIsLoading] = useState(true);
    const [shouldSkipValidSteps, setShouldSkipValidSteps] = useState(false);
    const viewModelService = useContext(ViewModelServiceContext);
    const { location, history } = props;

    const [qnbAuthHeader] = useState(() => {
        // sessionStorage survives StrictMode's double-invocation of state initializers.
        // First invocation: reads hash, stores token, clears hash (result discarded by StrictMode).
        // Second invocation: reads from sessionStorage, removes it, returns the token.
        const stored = sessionStorage.getItem('qnbToken');
        if (stored) {
            sessionStorage.removeItem('qnbToken');
            return { Authorization: `Bearer ${stored}` };
        }
        const fragment = window.location.hash;
        if (fragment) {
            const hashParams = new URLSearchParams(fragment.slice(1));
            const token = hashParams.get('t');
            if (token) {
                const decoded = decodeURIComponent(token);
                window.history.replaceState({}, '', window.location.pathname + window.location.search);
                sessionStorage.setItem('qnbToken', decoded);
                return { Authorization: `Bearer ${decoded}` };
            }
        }
        return {};
    });

    useEffect(() => {
        const viewModelContext = {
            AccountEmailRequired: true,
            AccountDOBRequired: true
        };

        let submissionPromise;

        if (!viewModelService) {
            if (_.isEmpty(location.search)) {
                history.push('/');
            }

            return;
        }

        if (_.has(location, 'state.address')) {
            const { address } = location.state;
            submissionPromise = createSubmissionOnPartialAddress(address);
        } else if (_.has(location, 'state.submission')) {
            const { submission } = location.state;
            setShouldSkipValidSteps(true);
            submissionPromise = Promise.resolve(submission);
        } else if (_.has(location, 'search')) {
            const parsedParms = queryString.parse(location.search);
            submissionPromise = LoadSaveService.retrieveAccountSubmission(parsedParms, qnbAuthHeader);
        } else {
            return;
        }
        submissionPromise
            .then((response) => {
                const submissionVM = viewModelService.create(
                    removeMockData(response),
                    'pc',
                    'edge.capabilities.quote.submission.dto.QuoteDataDTO',
                    viewModelContext
                );

                setInitialSubmission(submissionVM);
                setIsLoading(false);
            })
            .catch(() => {
                setIsLoading(false);
                history.push('/contact-us');
            });
        // only execute this once per component lifecycle
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [viewModelService]);
...

PAWizard changes

The following changes need to be made to applications/common/capabilities-react/gw-capability-quoteandbind-pa-react/PAWizard.jsx:

Original file content:
function createSubmissionOnPartialAddress(address) {
    const data = setPrepData(address);
    return LoadSaveService.createSubmission(data);
}

function PAWizard(props) {
    const {
        showConfirm,
        showAlert
    } = useModal();

    const { steps, title } = wizardConfig;
    const [initialSubmission, setInitialSubmission] = useState(null);
    const [isLoading, setIsLoading] = useState(true);
    const [shouldSkipValidSteps, setShouldSkipValidSteps] = useState(false);
    const viewModelService = useContext(ViewModelServiceContext);
    const { location, history } = props;
    useEffect(() => {
        const viewModelContext = {
            AccountEmailRequired: true,
            DriverEmailRequired: true,
            AccountDOBRequired: true
        };

        if (!viewModelService) {
            if (_.isEmpty(location.search)) {
                history.push('/');
            }

            return;
        }

        let submissionPromise;
        if (_.has(location, 'state.address')) {
            const { address } = location.state;
            submissionPromise = createSubmissionOnPartialAddress(address);
        } else if (_.has(location, 'state.submission')) {
            const { submission } = location.state;
            setShouldSkipValidSteps(true);
            submissionPromise = Promise.resolve(submission);
        } else if (_.has(location, 'search')) {
            const parsedParms = queryString.parse(location.search);
            submissionPromise = LoadSaveService.retrieveAccountSubmission(parsedParms);
        } else {
            return;
        }

        submissionPromise.then((response) => {
            const submissionVM = viewModelService.create(
                removeMockData(response),
                'pc',
                'edge.capabilities.quote.submission.dto.QuoteDataDTO',
                viewModelContext
            );

            setInitialSubmission(submissionVM);
            setIsLoading(false);
        });
        // only execute this once per component lifecycle
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [viewModelService]);
...
Updated file content:
function createSubmissionOnPartialAddress(address) {
    const data = setPrepData(address);
    return LoadSaveService.createSubmission(data);
}

function PAWizard(props) {
    const {
        showConfirm,
        showAlert
    } = useModal();

    const { steps, title } = wizardConfig;
    const [initialSubmission, setInitialSubmission] = useState(null);
    const [isLoading, setIsLoading] = useState(true);
    const [shouldSkipValidSteps, setShouldSkipValidSteps] = useState(false);
    const viewModelService = useContext(ViewModelServiceContext);
    const { location, history } = props;

    const [qnbAuthHeader] = useState(() => {
      // sessionStorage survives StrictMode's double-invocation of state initializers.
      // First invocation: reads hash, stores token, clears hash (result discarded by StrictMode).
      // Second invocation: reads from sessionStorage, removes it, returns the token.
      const stored = sessionStorage.getItem('qnbToken');
      if (stored) {
          sessionStorage.removeItem('qnbToken');
          return { Authorization: `Bearer ${stored}` };
      }
      const fragment = window.location.hash;
      if (fragment) {
          const hashParams = new URLSearchParams(fragment.slice(1));
          const token = hashParams.get('t');
          if (token) {
              const decoded = decodeURIComponent(token);
              window.history.replaceState({}, '', window.location.pathname + window.location.search);
              sessionStorage.setItem('qnbToken', decoded);
              return { Authorization: `Bearer ${decoded}` };
          }
      }
      return {};
  });

    useEffect(() => {
        const viewModelContext = {
            AccountEmailRequired: true,
            DriverEmailRequired: true,
            AccountDOBRequired: true
        };

        if (!viewModelService) {
            if (_.isEmpty(location.search)) {
                history.push('/');
            }

            return;
        }

        let submissionPromise;
        if (_.has(location, 'state.address')) {
            const { address } = location.state;
            submissionPromise = createSubmissionOnPartialAddress(address);
        } else if (_.has(location, 'state.submission')) {
            const { submission } = location.state;
            setShouldSkipValidSteps(true);
            submissionPromise = Promise.resolve(submission);
        } else if (_.has(location, 'search')) {
              const parsedParms = queryString.parse(location.search);
              submissionPromise = LoadSaveService.retrieveAccountSubmission(parsedParms, qnbAuthHeader);
          } 
          submissionPromise
              .then((response) => {
                  const submissionVM = viewModelService.create(
                      removeMockData(response),
                      'pc',
                      'edge.capabilities.quote.submission.dto.QuoteDataDTO',
                      viewModelContext
                  );
                  setInitialSubmission(submissionVM);
                  setIsLoading(false);
              })
              .catch(() => {
                  setIsLoading(false);
                  history.push('/contact-us');
              });
        // only execute this once per component lifecycle
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [viewModelService]);
...

Updated package versions

Guidewire has modified the configuration of certain package versions for improved product security. Customers who maintain customized back-end deployments must apply these changes manually.

JDK18on package update

  • Package: org.bouncycastle:bcprov-jdk18on
  • Updated version: 1.84
  • File changed: Capabilities/gpa-policy/pom.xml
  • Change made:
    <dependency>
       <groupId>org.bouncycastle</groupId>
       <artifactId>bcprov-jdk18on</artifactId>
    -            <version>1.78</version>
    +            <version>1.84</version>
     </dependency>

Nimbus JWT package update

  • Package: com.nimbusds:nimbus-jose-jwt
  • Updated version: 9.37.4
  • File changed: Platform/pom.xml
  • Change made:
    <dependency>
       <groupId>com.nimbusds</groupId>
       <artifactId>nimbus-jose-jwt</artifactId>
    -            <version>9.37.2</version>
    +            <version>9.37.4</version>
     </dependency>

2025.11.2 release

This section lists all of the changes added in patch 2 of the 2025.11 release.

Response header configuration changes

InsuranceSuite application affected: PolicyCenter, ClaimCenter

Digital reference application affected: CustomerEngage Account Management, CustomerEngage Account Management for ClaimCenter, ProducerEngage, ProducerEngage for ClaimCenter, ServiceRepEngage

Guidewire has modified the configuration of certain response headers so that file uploads are better sanitized. There are steps that you can take to have better sanitized file uploads and need to manually add the changes to your own files. The changes need to be added to the following files:
  • gsrc/edge/capabilities/gateway/document/DocumentUploadHandler.gs
  • gsrc/edge/capabilities/policy/document/PolicyDocumentUploadHandler.gs
  • gsrc/edge/capabilities/granite_on_cloud-servicerequest/document/ServiceRequestFileUploadHandler.gs
  • gsrc/edge/security/fileupload/FileUploadUtil.gs
  • gsrc/edge/capabilities/claim/document/ClaimDocumentUploadHandler.gs‎

Changes to the document upload handler

The following sections show the original and updated code. Bold lines in each code block indicate where the changes were made for this fix. Make these changes in DocumentUploadHandler.gs:

The following codeblock contains the original file content. Highlighted text indicates content removed or changed in the updated file:
package edge.capabilities.gateway.document

uses edge.jsonrpc.AbstractRpcHandler
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.annotation.JsonRpcRunAsInternalGWUser
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.document.dto.DocumentBaseDTO
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses org.apache.commons.fileupload2.core.FileItem
uses gw.plugin.document.IDocumentContentSource
uses gw.plugin.Plugins
uses edge.capabilities.gateway.document.dto.DocumentDTO
uses edge.di.annotations.InjectableNode
uses edge.PlatformSupport.Bundle
uses edge.security.fileupload.IFileUploadPlugin
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.security.authorization.exception.AuthorizationException
uses edge.security.fileupload.exception.IllegalContentTypeException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey

class DocumentUploadHandler extends AbstractRpcHandler {

  var _documentPlugin : IDocumentPlugin
  private var _documentSessionPlugin : IDocumentSessionPlugin
  private var _fileUploadPlugin : IFileUploadPlugin

  @InjectableNode
  @Param("documentSessionPlugin", "Plugin used to deal with document sessions")
  @Param("fileUploadPlugin", "Plugin used to verify file upload validity")
  construct(aDocumentPlugin : IDocumentPlugin,
  documentSessionPlugin : IDocumentSessionPlugin,
  fileUploadPlugin : IFileUploadPlugin){
    this._documentPlugin = aDocumentPlugin
    this._documentSessionPlugin = documentSessionPlugin
    this._fileUploadPlugin = fileUploadPlugin
  }

  /**
   * Uploads a document
   *
   * <dl>
   *   <dt>Calls:</dt>
   * <dd> <code>IDocumentPlugin#createDocument(DocumentDTO)</code> - To create a document</dd>
   * <dd> <code>IDocumentPlugin#toDTO(Document)</code> - To return DTO with document </dd>
   * <dd> <code>IDocumentContentSource#addDocument(InputStream, Document)</code> - To add Document</dd>
   * <dt>Throws:</dt>
   *  <dd><code>IllegalArgumentException</code> - If claim number associated with the document is null or empty</dd>
   *  <dd><code>EntityNotFoundException</code> - If no claim is found</dd>
   *  <dd><code>AuthorizationException</code> - If the portal user has no access to the claim</dd>
   * </dl>
   * @param documentDTO
   * @param fileItem
   * @return document metadata response
   */
  @JsonRpcRunAsInternalGWUser
  @JsonRpcMethod
  @ApidocMethodDescription("Uploads a document. NOTE: Given a list of file items, it expects that the element " +
      "at [0] is the document metadata; at [1] is the item to be uploaded")
  @ApidocAvailableSince("5.0")
  function upload(documentDto: DocumentDTO, documentFile: FileItem): DocumentBaseDTO {

    try {
      // validate upload token is still valid
      _documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
    } catch (ex : JsonRpcSecurityException){
      throw new AuthorizationException(){:Message = "Unauthorized portal access"}
    }

    if(documentDto.Name!=documentFile.Name){
      throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
    }

    if(documentDto.MimeType!=documentFile.ContentType) {
      throw new IllegalContentTypeException(DisplayKey.get("Document.upload.contentType.error", documentFile.ContentType))
    }

    if (!_fileUploadPlugin.canUploadContentType(documentFile)) {
      throw new IllegalContentTypeException("Cannot upload files of content type: " + documentFile.ContentType)
    }

    /** Some browsers (or fileupload library on some browsers) do not populate this field). */
    if (documentDto.MimeType == null) {
      documentDto.MimeType = documentFile.ContentType
    }

    final var doc = Bundle.resolveInTransaction( \ bundle -> {
      try {
        final var _doc = _documentPlugin.createDocument(documentDto)
        // add the document to the dms
        Plugins.get(IDocumentContentSource).addDocument(documentFile.InputStream, _doc)
        return _doc
      } finally {
        documentFile.InputStream.close()
      }
    })
    return _documentPlugin.toDTO(doc)
  }
}
The following codeblock contains the updated file content. Highlighted text indicates updates:
package edge.capabilities.gateway.document

uses edge.jsonrpc.AbstractRpcHandler
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.annotation.JsonRpcRunAsInternalGWUser
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.document.dto.DocumentBaseDTO
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses org.apache.commons.fileupload2.core.FileItem
uses gw.plugin.document.IDocumentContentSource
uses gw.plugin.Plugins
uses edge.capabilities.gateway.document.dto.DocumentDTO
uses edge.di.annotations.InjectableNode
uses edge.PlatformSupport.Bundle
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.security.authorization.exception.AuthorizationException
uses edge.security.fileupload.exception.IllegalContentTypeException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey
uses org.apache.tika.Tika
uses gw.api.util.Logger
uses edge.security.fileupload.DefaultFileUploadPlugin
uses edge.security.fileupload.FileUploadUtil

class DocumentUploadHandler extends AbstractRpcHandler {

  private static final var LOG = Logger.forCategory(DocumentUploadHandler.Type.QName)
  private static final var TIKA = new Tika()
    var _documentPlugin : IDocumentPlugin
    private var _documentSessionPlugin : IDocumentSessionPlugin
    static final var MIME_PNG = "image/png"
    static final var MIME_JPEG = "image/jpeg"
    static final var MIME_PDF = "application/pdf"
  
    @InjectableNode
    @Param("documentSessionPlugin", "Plugin used to deal with document sessions")
    construct(aDocumentPlugin : IDocumentPlugin,
 documentSessionPlugin : IDocumentSessionPlugin){
      this._documentPlugin = aDocumentPlugin
      this._documentSessionPlugin = documentSessionPlugin
    }
  
    /**
     * Uploads a document
     *
     * <dl>
     *   <dt>Calls:</dt>
     * <dd> <code>IDocumentPlugin#createDocument(DocumentDTO)</code> - To create a document</dd>
     * <dd> <code>IDocumentPlugin#toDTO(Document)</code> - To return DTO with document </dd>
     * <dd> <code>IDocumentContentSource#addDocument(InputStream, Document)</code> - To add Document</dd>
     * <dt>Throws:</dt>
     *  <dd><code>IllegalArgumentException</code> - If claim number associated with the document is null or empty</dd>
     *  <dd><code>EntityNotFoundException</code> - If no claim is found</dd>
     *  <dd><code>AuthorizationException</code> - If the portal user has no access to the claim</dd>
     * </dl>
     * @param documentDTO
     * @param fileItem
     * @return document metadata response
     */
    @JsonRpcRunAsInternalGWUser
    @JsonRpcMethod
    @ApidocMethodDescription("Uploads a document. NOTE: Given a list of file items, it expects that the element " +
        "at [0] is the document metadata; at [1] is the item to be uploaded")
    @ApidocAvailableSince("5.0")
    function upload(documentDto: DocumentDTO, documentFile: FileItem): DocumentBaseDTO {
  
      try {
        // validate upload token is still valid
        _documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
      } catch (ex : JsonRpcSecurityException){
        throw new AuthorizationException(){:Message = "Unauthorized portal access"}
      }
  
      if (documentDto.Name != documentFile.Name) {
        throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
      }

      FileUploadUtil.assertSafeFileName(documentDto.Name)
      FileUploadUtil.assertSafeFileName(documentFile.Name)
  
      if (documentDto.MimeType == null) {
        documentDto.MimeType = documentFile.ContentType
      }
  
      FileUploadUtil.enforceMaxUploadSize(documentFile)
  
      var bytes = FileUploadUtil.readAllBytesAndCloseWithLimit(documentFile, FileUploadUtil.getMaxUploadBytes())
  
      // Pass filename so we can disambiguate OOXML when Tika reports application/zip.
      var detectedType = FileUploadUtil.normalizeMimeType(TIKA.detect(bytes), documentFile.Name)
  
      FileUploadUtil.enforcePdfNameConsistency(documentFile.Name, detectedType, MIME_PDF)
      FileUploadUtil.assertExtensionMatchesType(documentFile.Name, detectedType)

      if (!isAllowedDetectedType(detectedType)) {
        throw new IllegalContentTypeException("Cannot upload files of detected content type: " + detectedType)
      }
  
      FileUploadUtil.validateContent(detectedType, bytes, MIME_PDF, MIME_PNG, MIME_JPEG)
  
      documentDto.MimeType = detectedType
  
      final var doc = Bundle.resolveInTransaction(\ bundle -> {
        final var _doc = _documentPlugin.createDocument(documentDto)
        var uploadInputStream = new java.io.ByteArrayInputStream(bytes)
        try {
          Plugins.get(IDocumentContentSource).addDocument(uploadInputStream, _doc)
        } finally {
          uploadInputStream.close()
        }
        return _doc
      })
  
      return _documentPlugin.toDTO(doc)
    }
  
    private static function isAllowedDetectedType(detectedType : String) : boolean {
      return new DefaultFileUploadPlugin().canUploadContentType(detectedType)
  }
}

Changes to the policy document upload handler

The following sections show the original and updated code. Bold lines in each code block indicate where the changes were made for this fix. Make these changes in PolicyDocumentUploadHandler.gs:

The following codeblock contains the original file content. Highlighted text indicates content removed or changed in the updated file:
package edge.capabilities.policy.document

uses edge.jsonrpc.AbstractRpcHandler
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses org.apache.commons.fileupload2.core.FileItem
uses edge.di.annotations.InjectableNode
uses gw.api.webservice.exception.BadIdentifierException
uses edge.capabilities.policy.auth.IPolicyAccessPlugin
uses edge.security.authorization.exception.NoAuthorityException
uses edge.capabilities.policy.document.dto.PolicyDocumentDTO
uses gw.plugin.Plugins
uses gw.plugin.document.IDocumentContentSource
uses edge.capabilities.policy.document.dto.PolicyDocumentUploadDTO
uses edge.PlatformSupport.Bundle
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.security.authorization.exception.AuthorizationException
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.policy.util.PolicyUtil
uses edge.security.fileupload.exception.IllegalContentTypeException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey
uses edge.security.fileupload.IFileUploadPlugin

/**
 * Handler of document uploads.
 * It is needed to work around "security through obscurity" in the authz service and its inability
 * to cope with many different access modes for the same URL. It is also a workaround for a third-party component
 * which is also inflexible and could not accommodate different transports.
 */
class PolicyDocumentUploadHandler extends AbstractRpcHandler {
  private var _policyAuthCheck : IPolicyAccessPlugin
  private var _documentPlugin : IPolicyDocumentsPlugin
  private var _documentSessionPlugin : IDocumentSessionPlugin
  private var _fileUploadPlugin : IFileUploadPlugin

  @InjectableNode
  @Param("policyAuthCheck", "Policy access policy")
  @Param("documentPlugin", "Plugin used to access documents associated with the policy")
  @Param("documentSessionPlugin", "Document session management plugin")
  construct(policyAuthCheck : IPolicyAccessPlugin, documentPlugin : IPolicyDocumentsPlugin,
  documentSessionPlugin : IDocumentSessionPlugin, fileUploadPlugin : IFileUploadPlugin) {
    this._policyAuthCheck = policyAuthCheck
    this._documentPlugin = documentPlugin
    this._documentSessionPlugin = documentSessionPlugin
    this._fileUploadPlugin = fileUploadPlugin
  }


  @JsonRpcMethod
  @ApidocMethodDescription("Uploads a document. " +
      "NOTE: Given a list of file items, it expects that the element at [0] is the document metadata and at [1] is the item to be uploaded.")
  @ApidocAvailableSince("5.0")
  function upload(documentDto:PolicyDocumentUploadDTO, documentFile: FileItem) : PolicyDocumentDTO {
    try {
      try {
        _documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
      } catch (ex : JsonRpcSecurityException) {
        throw new AuthorizationException(){:Message = "Unauthorized portal access"}
      }

      if(documentDto.Name!=documentFile.Name){
        throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
      }

      if(documentDto.MimeType!=documentFile.ContentType) {
        throw new IllegalContentTypeException(DisplayKey.get("Document.upload.contentType.error", documentFile.ContentType))
      }

      if (!_fileUploadPlugin.canUploadContentType(documentFile)) {
        throw new IllegalContentTypeException("Cannot upload files of content type: " + documentFile.ContentType)
      }

      var policyPeriod = PolicyUtil.getLatestPolicyPeriodByPolicyNumber(documentDto.PolicyNumber)

      if (policyPeriod == null) {
        throw new BadIdentifierException("Bad policy number " + documentDto.PolicyNumber)
      }

      if (!_policyAuthCheck.hasAccess(policyPeriod)) {
        throw new NoAuthorityException()
      }


      final var res = Bundle.resolveInTransaction<Document>(\ bundle -> {
        policyPeriod = bundle.add(policyPeriod)
        final var doc = _documentPlugin.createDocumentMetadata(policyPeriod, documentDto)
        Plugins.get(IDocumentContentSource).addDocument(documentFile.InputStream, doc)
        return doc
      })

      /* Should do this outside the transactions as public ID is not set inside the bundle. */
      return _documentPlugin.getDocumentDetails(res)
    } finally {
      documentFile.InputStream.close()
    }
  }
}
The following codeblock contains the updated file content. Highlighted text indicates updates:
package edge.capabilities.policy.document

uses edge.jsonrpc.AbstractRpcHandler
uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses org.apache.commons.fileupload2.core.FileItem
uses edge.di.annotations.InjectableNode
uses gw.api.webservice.exception.BadIdentifierException
uses edge.capabilities.policy.auth.IPolicyAccessPlugin
uses edge.security.authorization.exception.NoAuthorityException
uses edge.capabilities.policy.document.dto.PolicyDocumentDTO
uses gw.plugin.Plugins
uses gw.plugin.document.IDocumentContentSource
uses edge.capabilities.policy.document.dto.PolicyDocumentUploadDTO
uses edge.PlatformSupport.Bundle
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.security.authorization.exception.AuthorizationException
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.policy.util.PolicyUtil
uses edge.security.fileupload.exception.IllegalContentTypeException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey
uses org.apache.tika.Tika
uses edge.security.fileupload.FileUploadUtil
uses edge.security.fileupload.DefaultFileUploadPlugin

/**
 * Handler of document uploads.
 * It is needed to work around "security through obscurity" in the authz service and its inability
 * to cope with many different access modes for the same URL. It is also a workaround for a third-party component
 * which is also inflexible and could not accommodate different transports.
 */
class PolicyDocumentUploadHandler extends AbstractRpcHandler {
  private var _policyAuthCheck : IPolicyAccessPlugin
  private var _documentPlugin : IPolicyDocumentsPlugin
  private var _documentSessionPlugin : IDocumentSessionPlugin
  private static final var TIKA = new Tika()
  static final var MIME_PNG = "image/png"
  static final var MIME_JPEG = "image/jpeg"
  static final var MIME_PDF = "application/pdf"

  @InjectableNode
  @Param("policyAuthCheck", "Policy access policy")
  @Param("documentPlugin", "Plugin used to access documents associated with the policy")
  @Param("documentSessionPlugin", "Document session management plugin")
  construct(policyAuthCheck : IPolicyAccessPlugin, documentPlugin : IPolicyDocumentsPlugin,
documentSessionPlugin : IDocumentSessionPlugin) {
    this._policyAuthCheck = policyAuthCheck
    this._documentPlugin = documentPlugin
    this._documentSessionPlugin = documentSessionPlugin
  }

  @JsonRpcMethod
  @ApidocMethodDescription("Uploads a document. " +
      "NOTE: Given a list of file items, it expects that the element at [0] is the document metadata and at [1] is the item to be uploaded.")
  @ApidocAvailableSince("5.0")
  function upload(documentDto:PolicyDocumentUploadDTO, documentFile: FileItem) : PolicyDocumentDTO {
    try {
      try {
        _documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
      } catch (ex : JsonRpcSecurityException) {
        throw new AuthorizationException(){:Message = "Unauthorized portal access"}
      }

      if(documentDto.Name != documentFile.Name){
        throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
      }

         FileUploadUtil.assertSafeFileName(documentDto.Name)
         FileUploadUtil.assertSafeFileName(documentFile.Name)

      // Enforce max upload size
      FileUploadUtil.enforceMaxUploadSize(documentFile)

      // Read file bytes with limit
      var bytes = FileUploadUtil.readAllBytesAndCloseWithLimit(documentFile, FileUploadUtil.getMaxUploadBytes())

      // Detect and normalize MIME type
      var detectedType = FileUploadUtil.normalizeMimeType(TIKA.detect(bytes), documentFile.Name)

      // Enforce PDF name/content consistency
      FileUploadUtil.enforcePdfNameConsistency(documentFile.Name, detectedType, MIME_PDF)

      // Cross-check filename extension against bytes-detected MIME
      FileUploadUtil.assertExtensionMatchesType(documentFile.Name, detectedType)      

      // Centralized allowed content type check
      if (!new DefaultFileUploadPlugin().canUploadContentType(detectedType)) {
        throw new IllegalContentTypeException("Cannot upload files of detected content type: " + detectedType)
      }

      // Validate PDF/image content
      FileUploadUtil.validateContent(detectedType, bytes, MIME_PDF, MIME_PNG, MIME_JPEG)

      // Persist the validated MIME on the DTO so stored metadata matches
      documentDto.MimeType = detectedType

      var policyPeriod = PolicyUtil.getLatestPolicyPeriodByPolicyNumber(documentDto.PolicyNumber)
      if (policyPeriod == null) {
        throw new BadIdentifierException("Bad policy number " + documentDto.PolicyNumber)
      }

      if (!_policyAuthCheck.hasAccess(policyPeriod)) {
        throw new NoAuthorityException()
      }

      final var res = Bundle.resolveInTransaction<Document>(\ bundle -> {
        policyPeriod = bundle.add(policyPeriod)
        final var doc = _documentPlugin.createDocumentMetadata(policyPeriod, documentDto)
        Plugins.get(IDocumentContentSource).addDocument(new java.io.ByteArrayInputStream(bytes), doc)
        return doc
      })

      /* Should do this outside the transactions as public ID is not set inside the bundle. */
      return _documentPlugin.getDocumentDetails(res)
    } finally {
      documentFile.InputStream.close()
    }
  }
}

Changes to the service request file upload handler

Make these changes in gsrc/edge/capabilities/granite_on_cloud-servicerequest/document/ServiceRequestFileUploadHandler.gs:

The following codeblock contains the original file content. Highlighted text indicates content removed or changed in the updated file:
package edge.capabilities.servicerequest.document

uses edge.capabilities.servicerequest.document.dto.ServiceRequestDocumentDTO
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses edge.security.fileupload.IFileUploadPlugin
uses edge.security.fileupload.exception.IllegalContentTypeException
uses org.apache.commons.fileupload2.core.FileItem
uses edge.jsonrpc.annotation.JsonRpcMethod
uses gw.webservice.cc.cc1000.vendormanagement.DocumentContent
uses gw.api.webservice.exception.BadIdentifierException
uses edge.capabilities.servicerequest.local.IServiceRequestRetrievalPlugin
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.security.authorization.Authorizer
uses edge.di.annotations.InjectableNode
uses edge.security.authorization.exception.AuthorizationException
uses edge.PlatformSupport.Bundle
uses gw.plugin.Plugins
uses gw.plugin.document.IDocumentContentSource
uses edge.jsonrpc.AbstractRpcHandler
uses edge.security.authorization.exception.NoAuthorityException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey

class ServiceRequestFileUploadHandler extends AbstractRpcHandler {

  private var _documentPlugin : IServiceRequestDocumentPlugin
  private var _retrievalPlugin : IServiceRequestRetrievalPlugin
  private var _fileUploadPlugin: IFileUploadPlugin
  private var _documentSessionPlugin : IDocumentSessionPlugin
  private var _authz : Authorizer<Document>

  @InjectableNode
  @Param("documentPlugin", "Plugin used to deal with claim documents")
  @Param("documentSessionPlugin", "Plugin used to deal with document sessions")
  @Param("retrievalPlugin", "Plugin used to access service requests")
  @Param("fileUploadPugin", "Plugin used to validate file validity")
  public construct(documentPlugin : IServiceRequestDocumentPlugin,
  documentSessionPlugin : IDocumentSessionPlugin,
  docAuthz:Authorizer<Document>,
  retrievalPlugin : IServiceRequestRetrievalPlugin,
  fileUploadPlugin: IFileUploadPlugin) {
    this._documentPlugin = documentPlugin
    this._documentSessionPlugin = documentSessionPlugin
    this._retrievalPlugin = retrievalPlugin
    this._authz = docAuthz
    this._fileUploadPlugin = fileUploadPlugin
  }

  /**
   * Uploads a document
   *
   * Given a list of file items, it expects that the element
   * <ul>
   * <li> at [0] is the document metadata </li>
   * <li> at [1] is the item to be uploaded </li>
   * </ul>
   *
   * </br>
   * Throws -
   * <ul>
   * <li>IllegalArgumentException If id number associated with the document is null or empty</li>
   * <li>EntityNotFoundException If no service request statement is found</li>
   * <li>AuthorizationException If the portal user has no access to the service request statement</li>
   * </ul>
   *
   * @param list of file items
   * @return document metadata response
   */
  @JsonRpcMethod
  function upload(documentDto:ServiceRequestDocumentDTO, documentFile: FileItem) : ServiceRequestDocumentDTO {
    try {
      try {
        // validate document upload token
        _documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
      } catch (ex : JsonRpcSecurityException) {
        throw new AuthorizationException(){:Message = "Unauthorized portal access"}
      }

      if(documentDto.Name!=documentFile.Name){
        throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
      }

      if(documentDto.MimeType!=documentFile.ContentType) {
        throw new IllegalContentTypeException(DisplayKey.get("Document.upload.contentType.error", documentFile.ContentType))
      }

      if (!_fileUploadPlugin.canUploadContentType(documentFile)) {
        throw new IllegalContentTypeException("Cannot upload files of content type: " + documentFile.ContentType)
      }
      var serviceRequest = _retrievalPlugin.getServiceRequestFromPublicId(documentDto.ServiceRequestId)

      if (serviceRequest == null) {
        throw new BadIdentifierException("Bad service request id " + documentDto.ServiceRequestId)
      }

      final var res = Bundle.resolveInTransaction<Document>(\ bundle -> {
        serviceRequest = bundle.add(serviceRequest)
        var docContent = new DocumentContent(){
            :Content =  documentFile.get(),
            :MimeType = documentDto.MimeType
        };

        final var doc = _documentPlugin.createDocumentMetadata(bundle, serviceRequest, documentDto, docContent)
        if ( !_authz.canAccess(doc) ) {
          throw new NoAuthorityException()
        }
        Plugins.get(IDocumentContentSource).addDocument(documentFile.InputStream, doc)
        return doc
      })

      return  _documentPlugin.toDTO(res)
    } finally {
      documentFile.InputStream.close()
    }
  }
}
The following codeblock contains the updated file content. Highlighted text indicates updates:
package edge.capabilities.servicerequest.document

uses edge.capabilities.servicerequest.document.dto.ServiceRequestDocumentDTO
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses edge.security.fileupload.IFileUploadPlugin
uses edge.security.fileupload.exception.IllegalContentTypeException
uses org.apache.commons.fileupload2.core.FileItem
uses edge.jsonrpc.annotation.JsonRpcMethod
uses gw.webservice.cc.cc1000.vendormanagement.DocumentContent
uses gw.api.webservice.exception.BadIdentifierException
uses edge.capabilities.servicerequest.local.IServiceRequestRetrievalPlugin
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.security.authorization.Authorizer
uses edge.di.annotations.InjectableNode
uses edge.security.authorization.exception.AuthorizationException
uses edge.PlatformSupport.Bundle
uses gw.plugin.Plugins
uses gw.plugin.document.IDocumentContentSource
uses edge.jsonrpc.AbstractRpcHandler
uses edge.security.authorization.exception.NoAuthorityException
uses edge.security.fileupload.FileUploadUtil
uses org.apache.tika.Tika

class ServiceRequestFileUploadHandler extends AbstractRpcHandler {

  private var _documentPlugin : IServiceRequestDocumentPlugin
  private var _retrievalPlugin : IServiceRequestRetrievalPlugin
  private var _fileUploadPlugin: IFileUploadPlugin
  private var _documentSessionPlugin : IDocumentSessionPlugin
  private var _authz : Authorizer<Document>
  private static final var TIKA = new Tika()
  static final var MIME_PNG = "image/png"
  static final var MIME_JPEG = "image/jpeg"
  static final var MIME_PDF = "application/pdf"

  @InjectableNode
  @Param("documentPlugin", "Plugin used to deal with claim documents")
  @Param("documentSessionPlugin", "Plugin used to deal with document sessions")
  @Param("retrievalPlugin", "Plugin used to access service requests")
  @Param("fileUploadPugin", "Plugin used to validate file validity")
  public construct(documentPlugin : IServiceRequestDocumentPlugin,
  documentSessionPlugin : IDocumentSessionPlugin,
  docAuthz:Authorizer<Document>,
  retrievalPlugin : IServiceRequestRetrievalPlugin,
  fileUploadPlugin: IFileUploadPlugin) {
    this._documentPlugin = documentPlugin
    this._documentSessionPlugin = documentSessionPlugin
    this._retrievalPlugin = retrievalPlugin
    this._authz = docAuthz
    this._fileUploadPlugin = fileUploadPlugin
  }

  /**
   * Uploads a document
   *
   * Given a list of file items, it expects that the element
   * <ul>
   * <li> at [0] is the document metadata </li>
   * <li> at [1] is the item to be uploaded </li>
   * </ul>
   *
   * </br>
   * Throws -
   * <ul>
   * <li>IllegalArgumentException If id number associated with the document is null or empty</li>
   * <li>EntityNotFoundException If no service request statement is found</li>
   * <li>AuthorizationException If the portal user has no access to the service request statement</li>
   * </ul>
   *
   * @param list of file items
   * @return document metadata response
   */
  @JsonRpcMethod
  function upload(documentDto:ServiceRequestDocumentDTO, documentFile: FileItem) : ServiceRequestDocumentDTO {
    try {
      try {
        // validate document upload token
        _documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
      } catch (ex : JsonRpcSecurityException) {
        throw new AuthorizationException(){:Message = "Unauthorized portal access"}
      }

      // Reject names with control / bidi / non-printable chars
      FileUploadUtil.assertSafeFileName(documentDto.Name)
      FileUploadUtil.assertSafeFileName(documentFile.Name)

      FileUploadUtil.enforceMaxUploadSize(documentFile)
      var bytes = FileUploadUtil.readAllBytesAndCloseWithLimit(documentFile, FileUploadUtil.getMaxUploadBytes())
      var detectedType = FileUploadUtil.normalizeMimeType(TIKA.detect(bytes), documentFile.Name)
      FileUploadUtil.enforcePdfNameConsistency(documentFile.Name, detectedType, MIME_PDF)
      FileUploadUtil.assertExtensionMatchesType(documentFile.Name, detectedType)

      if (!_fileUploadPlugin.canUploadContentType(detectedType)) {
        throw new IllegalContentTypeException("Cannot upload files of content type: " + detectedType)
      }

      FileUploadUtil.validateContent(detectedType, bytes, MIME_PDF, MIME_PNG, MIME_JPEG)

      documentDto.MimeType = detectedType

      var serviceRequest = _retrievalPlugin.getServiceRequestFromPublicId(documentDto.ServiceRequestId)

      if (serviceRequest == null) {
        throw new BadIdentifierException("Bad service request id " + documentDto.ServiceRequestId)
      }

      final var res = Bundle.resolveInTransaction<Document>(\ bundle -> {
        serviceRequest = bundle.add(serviceRequest)
        var docContent = new DocumentContent(){
            :Content =  bytes,
            :MimeType = detectedType
        };

        final var doc = _documentPlugin.createDocumentMetadata(bundle, serviceRequest, documentDto, docContent)
        if ( !_authz.canAccess(doc) ) {
          throw new NoAuthorityException()
        }
        var uploadInputStream = new java.io.ByteArrayInputStream(bytes)
        try {
          Plugins.get(IDocumentContentSource).addDocument(uploadInputStream, doc)
        } finally {
          uploadInputStream.close()
        }
        return doc
      })

      return  _documentPlugin.toDTO(res)
    } finally {
      documentFile.InputStream.close()
    }
  }
}

New file upload utility

A new FileUploadUtil.gs file has been added:

File contents:
package edge.security.fileupload

uses java.io.ByteArrayInputStream
uses javax.imageio.ImageIO
uses java.io.IOException
uses javax.imageio.IIOException
uses java.nio.charset.StandardCharsets
uses java.net.URLEncoder
uses java.util.regex.Pattern
uses edge.security.fileupload.exception.IllegalContentTypeException
uses gw.api.util.Logger
uses org.apache.commons.fileupload2.core.FileItem
uses java.io.ByteArrayOutputStream
uses org.apache.tika.Tika

public class FileUploadUtil {
  private static final var MAX_UPLOAD_BYTES = 25 * 1024 * 1024 // 25 MB
  private static final var LOG = Logger.forCategory(FileUploadUtil.Type.QName)
  private static final var TIKA = new Tika()

  // Unicode control / bidi / zero-width / non-character ranges that break
  // Content-Disposition serialization and enable RTL-override filename spoofing.
  // Disallowed at upload; stripped at download for any pre-existing stored names.
  // U+061C       Arabic Letter Mark (ALM, bidi formatter)
  // U+200B-200F  zero-width and bidi marks (ZWSP, ZWNJ, ZWJ, LRM, RLM)
  // U+202A-202E  bidi embedding/override (incl. RLO U+202E)
  // U+2066-2069  bidi isolates (LRI, RLI, FSI, PDI -- Trojan Source vectors)
  // U+2028-2029  line/paragraph separators
  // U+FEFF       BOM / zero-width no-break space
  // U+FFFD       replacement character
  // Plus all C0 (0x00-0x1F) and C1 (0x7F-0x9F) control chars.
  private static final var DISALLOWED_NAME_CHAR : String =
      "[\\u0000-\\u001F\\u007F-\\u009F\\u061C\\u200B-\\u200F\\u202A-\\u202E\\u2066-\\u2069\\u2028\\u2029\\uFEFF\\uFFFD]"
  private static final var DISALLOWED_NAME_PATTERN : Pattern = Pattern.compile(DISALLOWED_NAME_CHAR)

  // XENG-2487: characters that enable Content-Disposition parameter injection
  // (`;`), quoted-string breakouts (`"`, `'`), or HTML metachars (`<`, `>`).
  // The download-path header builder already escapes these; reject at upload
  // as defense in depth.
  private static final var HEADER_UNSAFE_CHAR : String = "[\";'<>]"
  private static final var HEADER_UNSAFE_PATTERN : Pattern = Pattern.compile(HEADER_UNSAFE_CHAR)

  private static final var DEFAULT_DOWNLOAD_FILENAME = "document"

  public static function getMaxUploadBytes() : int {
    return MAX_UPLOAD_BYTES
  }

  public static function enforceMaxUploadSize(documentFile : FileItem) {
    var size : long
    try {
      size = documentFile.Size
    } catch (e : Exception) {
      // Fail closed: a missing/unreadable size indicates a malformed upload that
      // we should reject rather than wave through with an unbounded read.
      LOG.warn("Rejected upload: could not determine file size: " + e)
      throw new IllegalContentTypeException("Uploaded file size could not be determined.")
    }
    if (size > MAX_UPLOAD_BYTES) {
      throw new IllegalContentTypeException("Uploaded file exceeds maximum allowed size.")
    }
  }

  public static function readAllBytesAndCloseWithLimit(documentFile : FileItem, maxBytes : int) : byte[] {
    var bos = new ByteArrayOutputStream()
    var is = documentFile.InputStream
    var buffer = new byte[8192]
    var total = 0
    try {
      while (true) {
        var read = is.read(buffer)
        if (read < 0) {
          break
        }
        total += read
        if (total > maxBytes) {
          throw new IllegalContentTypeException("Uploaded file exceeds maximum allowed size.")
        }
        bos.write(buffer, 0, read)
      }
      var bytes = bos.toByteArray()
      if (bytes.length == 0) {
        throw new IllegalContentTypeException("Uploaded file is empty.")
      }
      return bytes
    } finally {
      try {
        is.close()
      } finally {
        bos.close()
      }
    }
  }

  public static function normalizeMimeType(mime : String, fileName : String) : String {
    var m = mime?.trim()?.toLowerCase()
    // Both application/zip and application/x-tika-ooxml are opaque OOXML containers
    // at this detection depth. Use the file extension to identify the specific type.
    if (m == "application/zip" or m == "application/x-tika-ooxml") {
      var mapped = mapZipByExtension(fileName)
      if (mapped != null) {
        return mapped
      }
      // No recognized extension — fall through to standard normalization.
      // canUploadContentType will reject the unknown type.
    }
    // application/x-tika-msoffice is Tika's generic supertype for legacy OLE2 binary
    // Office formats (.doc, .xls, .ppt, .msg). Palisades upgraded Tika returns this
    // instead of the specific canonical MIME types that pre-date OOXML. Use the file
    // extension to map back to the canonical type so the allowlist check can match.
    if (m == "application/x-tika-msoffice") {
      var mapped = mapOleByExtension(fileName)
      if (mapped != null) {
        return mapped
      }
      // No recognized extension — fall through; canUploadContentType will reject it.
    }
    // Tika sniffs CSV bytes as text/plain (the content is plain text), but the
    // file extension implies text/csv. Normalize so assertExtensionMatchesType
    // and the allowlist check both pass.
    if (m == "text/plain") {
      var n = fileName?.trim()?.toLowerCase()
      if (n != null and n.endsWith(".csv")) {
        return "text/csv"
      }
    }
    // video/quicktime is Tika's catch-all for the ISO Base Media File Format (ISOBMFF)
    // container family. MP4 files whose ftyp major brand is not one of the specific
    // brands Tika has magic bytes for (mp41, mp42) are detected as video/quicktime even
    // though they carry a .mp4 extension. Use the file extension to resolve to the
    // correct specific type, mirroring the zip→OOXML and msoffice→OLE patterns above.
    if (m == "video/quicktime") {
      var mapped = mapQuicktimeByExtension(fileName)
      if (mapped != null) {
        return mapped
      }
      // .mov / .qt / unrecognised extension — stays video/quicktime.
    }
    return normalizeMimeType(mime)
  }

  public static function normalizeMimeType(mime : String) : String {
    if (mime == null) {
      return null
    }
    var m = mime.trim().toLowerCase()
    if (m == "image/jpg") {
      return "image/jpeg"
    }
    if (m == "application/x-pdf") {
      return "application/pdf"
    }
    // RFC 4337 defines application/mp4 as the generic MP4 container type.
    // Tika may return this from extension-based detection while returning video/mp4
    // from bytes-based detection (or vice versa). Normalize to video/mp4 so the
    // assertExtensionMatchesType comparison and the allowlist check both succeed.
    if (m == "application/mp4") {
      return "video/mp4"
    }
    // Tika 3.x canonical type for WAV is audio/vnd.wave; audio/x-wav and audio/wave
    // are registered aliases. The allowlist entry is audio/wav (the IANA short form).
    // Normalize all three aliases so the allowlist check and extension comparison succeed.
    if (m == "audio/vnd.wave" or m == "audio/x-wav" or m == "audio/wave") {
      return "audio/wav"
    }
    // image/x-bmp and image/x-ms-bmp are legacy aliases for image/bmp (IANA-registered
    // since RFC 7903). Tika 3.x returns the canonical image/bmp but normalize the aliases
    // as defence-in-depth in case a client or older detector path returns one.
    if (m == "image/x-bmp" or m == "image/x-ms-bmp") {
      return "image/bmp"
    }
    // application/x-tika-ooxml and application/x-tika-msoffice are intentionally NOT
    // resolved here — a filename is required. Use normalizeMimeType(mime, fileName).
    return m
  }

  private static function mapZipByExtension(fileName : String) : String {
    if (fileName == null) {
      return null
    }
    var n = fileName.trim().toLowerCase()
    if (n.endsWith(".docx")) {
      return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
    }
    if (n.endsWith(".xlsx")) {
      return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    }
    if (n.endsWith(".pptx")) {
      return "application/vnd.openxmlformats-officedocument.presentationml.presentation"
    }
    return null
  }

  private static function mapOleByExtension(fileName : String) : String {
    if (fileName == null) {
      return null
    }
    var n = fileName.trim().toLowerCase()
    if (n.endsWith(".doc") or n.endsWith(".dot")) {
      return "application/msword"
    }
    if (n.endsWith(".xls") or n.endsWith(".xlt") or n.endsWith(".xlm")) {
      return "application/vnd.ms-excel"
    }
    if (n.endsWith(".ppt") or n.endsWith(".pot") or n.endsWith(".pps")) {
      return "application/vnd.ms-powerpoint"
    }
    if (n.endsWith(".msg")) {
      return "application/vnd.ms-outlook"
    }
    return null
  }

  // Tika detects the QuickTime/ISOBMFF container family as video/quicktime when the
  // file's ftyp major brand does not match one of the more specific magic-byte patterns
  // (e.g. ftypmp41/mp42 for video/mp4). Use the file extension to resolve to the
  // correct allowlisted subtype. .mov and .qt stay as video/quicktime (their own glob
  // entries in Tika); anything unrecognised returns null so the caller falls through.
  private static function mapQuicktimeByExtension(fileName : String) : String {
    if (fileName == null) {
      return null
    }
    var n = fileName.trim().toLowerCase()
    if (n.endsWith(".mp4") or n.endsWith(".mp4v") or n.endsWith(".mpg4")) {
      return "video/mp4"
    }
    // .mov and .qt are the native QuickTime extensions — keep video/quicktime.
    if (n.endsWith(".mov") or n.endsWith(".qt")) {
      return "video/quicktime"
    }
    return null
  }

  public static function isDecodableImage(bytes : byte[]) : boolean {
    try {
      return ImageIO.read(new ByteArrayInputStream(bytes)) != null
    } catch (e : IIOException) {
      LOG.debug("Image decode failed")
      return false
    } catch (e : IOException) {
      LOG.debug("Image decode failed")
      return false
    } catch (e : RuntimeException) {
      LOG.debug("Image decode failed")
      return false
    }
  }

  public static function passesPdfSanityCheck(bytes : byte[]) : boolean {
    if (bytes == null or bytes.length < 10) {
      return false
    }
    var headerLen = Math.min(8, bytes.length)
    var header = new String(bytes, 0, headerLen, StandardCharsets.ISO_8859_1)
    if (!header.startsWith("%PDF-")) {
      return false
    }
    var tailWindow = Math.min(64 * 1024, bytes.length)
    var tail = new String(bytes, bytes.length - tailWindow, tailWindow, StandardCharsets.ISO_8859_1)
    var eofIndex = tail.lastIndexOf("%%EOF")
    if (eofIndex < 0) {
      return false
    }
    var startxrefIndex = tail.lastIndexOf("startxref")
    if (startxrefIndex < 0 or startxrefIndex > eofIndex) {
      return false
    }
    var hasClassicXref = tail.contains("\nxref") or tail.contains("\rxref")
    var hasClassicTrailer = tail.contains("trailer")
    var hasXrefStreamMarker = tail.contains("/XRef")
    return (hasClassicXref and hasClassicTrailer) or hasXrefStreamMarker
  }

  public static function enforcePdfNameConsistency(fileName : String, detectedType : String, mimePdf : String) {
    if (fileName == null) {
      return
    }
    var n = fileName.trim().toLowerCase()
    var looksLikePdf = n.endsWith(".pdf") or n.contains(".pdf.")
    if (looksLikePdf and detectedType != mimePdf) {
      LOG.info("Rejected upload: filename indicates PDF but detected content is not PDF")
      throw new IllegalContentTypeException("File name indicates a PDF, but the uploaded file is not a valid PDF.")
    }
  }

  public static function validateContent(detectedType : String, bytes : byte[], mimePdf : String, mimePng : String, mimeJpeg : String) {
    switch (detectedType) {
      case mimePdf:
        if (!passesPdfSanityCheck(bytes)) {
          LOG.info("Rejected PDF upload: invalid content")
          throw new IllegalContentTypeException("The uploaded PDF appears to be invalid or corrupted. Please re-save and try again.")
        }
        break
      case mimePng:
      case mimeJpeg:
        if (!isDecodableImage(bytes)) {
          LOG.info("Rejected image upload: invalid content")
          throw new IllegalContentTypeException("The uploaded image appears to be invalid or corrupted. Please re-save and try again.")
        }
        break
      default:
        break
    }
  }

  // Reject file names containing Unicode control / bidi / zero-width / non-printable chars.
  // These characters break Content-Disposition serialization (causing browsers to render
  // attachments inline) and enable RTL-override extension spoofing.
  public static function assertSafeFileName(fileName : String) {
    if (fileName == null or fileName.length == 0) {
      throw new IllegalContentTypeException("File name is required.")
    }
    var matcher = DISALLOWED_NAME_PATTERN.matcher(fileName)
    if (matcher.find()) {
      // Report the first offending char as U+XXXX. Never echo the raw character
      // into the log or exception message — control/bidi chars can break log
      // parsers and enable log-injection.
      var codePoint = formatCodePoint(fileName.charAt(matcher.start()) as int)
      LOG.info("Rejected upload: file name contains disallowed character " + codePoint)
      throw new IllegalContentTypeException(
          "File name contains disallowed control or formatting character (" + codePoint + ").")
    }
    // XENG-2487: reject header-injection / quote-breakout / HTML metachars.
    var injMatcher = HEADER_UNSAFE_PATTERN.matcher(fileName)
    if (injMatcher.find()) {
      var ch = fileName.charAt(injMatcher.start())
      LOG.info("Rejected upload: file name contains disallowed character '" + ch + "'")
      throw new IllegalContentTypeException("File name must not contain '" + ch + "'.")
    }
    // Reject path separators so a malicious name cannot influence storage paths.
    if (fileName.contains("/") or fileName.contains("\\")) {
      LOG.info("Rejected upload: file name contains path separator")
      throw new IllegalContentTypeException("File name must not contain path separators.")
    }
  }

  private static function formatCodePoint(code : int) : String {
    var hex = Integer.toHexString(code).toUpperCase()
    var sb = new StringBuilder("U+")
    for (i in 0..|(4 - hex.length)) {
      sb.append("0")
    }
    sb.append(hex)
    return sb.toString()
  }

  // Strip disallowed characters. Used as defense-in-depth on the download path so any
  // names stored before the upload-side fix do not break the Content-Disposition header.
  public static function sanitizeFileName(fileName : String) : String {
    if (fileName == null) {
      return DEFAULT_DOWNLOAD_FILENAME
    }
    var stripped = DISALLOWED_NAME_PATTERN.matcher(fileName).replaceAll("")
        .replaceAll("[\\\\/]", "_")
        .trim()
    return stripped.length == 0 ? DEFAULT_DOWNLOAD_FILENAME : stripped
  }

  // Build an RFC 6266 Content-Disposition header value: an ASCII-only quoted fallback
  // plus a UTF-8 percent-encoded `filename*` for compliant clients. Always returns
  // `attachment; ...` so the browser never renders the document inline.
  public static function buildContentDisposition(fileName : String) : String {
    var safeName = sanitizeFileName(fileName)
    var asciiFallback = toAsciiFallback(safeName)
    var utf8Encoded = percentEncodeUtf8(safeName)
    return "attachment; filename=\"" + asciiFallback + "\"; filename*=UTF-8''" + utf8Encoded
  }

  // Replace anything outside printable ASCII (0x20-0x7E) with "_" and backslash-escape
  // any embedded quote or backslash so the value is safe inside the quoted-string per RFC 6266.
  private static function toAsciiFallback(name : String) : String {
    var sb = new StringBuilder()
    for (i in 0..|name.length) {
      var code = (name.charAt(i) as int)
      if (code < 0x20 or code > 0x7E) {
        sb.append("_")
      } else if (code == 0x22 or code == 0x5C) { // " or \
        sb.append("\\").append(name.charAt(i))
      } else {
        sb.append(name.charAt(i))
      }
    }
    var result = sb.toString()
    return result.length == 0 ? DEFAULT_DOWNLOAD_FILENAME : result
  }

  private static function percentEncodeUtf8(name : String) : String {
    try {
      // URLEncoder follows application/x-www-form-urlencoded rules, which differ
      // from RFC 5987 attr-char in two places: spaces become "+" (we convert to
      // %20) and "*" is left literal (we percent-encode to %2A so the filename*
      // value stays RFC-compliant for filenames like "budget*final.pdf").
      return URLEncoder.encode(name, StandardCharsets.UTF_8.name())
          .replace("+", "%20")
          .replace("*", "%2A")
    } catch (e : java.io.UnsupportedEncodingException) {
      LOG.debug("UTF-8 encoding unavailable when building Content-Disposition: " + e)
      return DEFAULT_DOWNLOAD_FILENAME
    }
  }

  // Cross-check the filename's implied content type (from extension via Tika)
  // against the bytes-detected type. Catches the `evil.exe`-with-image-bytes
  // pattern where bytes pass the allow-list but the filename extension would
  // not. Skips the check when the extension is unknown to Tika so files like
  // README (no extension) still go through.
  public static function assertExtensionMatchesType(fileName : String, detectedType : String) {
    if (fileName == null) {
      return
    }
    var extType : String
    try {
      extType = TIKA.detect(fileName)
    } catch (e : Exception) {
      LOG.debug("Could not infer type from filename '" + fileName + "': " + e)
      return
    }
    if (extType == null or extType == "application/octet-stream") {
      return
    }
    var normalizedExt = normalizeMimeType(extType, fileName)
    if (normalizedExt != detectedType) {
      LOG.info("Rejected upload: filename extension implies " + normalizedExt
          + " but content sniffs as " + detectedType)
      throw new IllegalContentTypeException(
          "File extension does not match detected content type.")
    }
  }
}

Changes to the claim document upload handler

The following sections show the original and updated code. Bold lines in each code block indicate where the changes were made for this fix. Make these changes in ClaimDocumentUploadHandler.gs:

The following codeblock contains the original file content. Highlighted text indicates content removed or changed in the updated file:
package edge.capabilities.claim.document


uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses org.apache.commons.fileupload2.core.FileItem

uses edge.jsonrpc.AbstractRpcHandler
uses edge.di.annotations.InjectableNode
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.claim.local.IClaimRetrievalPlugin
uses gw.api.webservice.exception.BadIdentifierException
uses edge.PlatformSupport.Bundle
uses gw.plugin.Plugins
uses gw.plugin.document.IDocumentContentSource
uses edge.security.authorization.exception.AuthorizationException
uses edge.capabilities.claim.document.dto.ClaimDocumentUploadDTO
uses edge.capabilities.claim.document.dto.ClaimDocumentDTO
uses edge.capabilities.claim.document.IClaimDocumentPlugin
uses edge.security.fileupload.exception.IllegalContentTypeException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey
uses edge.security.fileupload.IFileUploadPlugin

/**
 * Handler of document uploads.
 * It is needed to work around "security through obscurity" in the authz service and its inability
 * to cope with many different access modes for the same URL. It is also a workaround for a third-party component
 * which is also inflexible and could not accommodate different transports.
 */
class ClaimDocumentUploadHandler extends AbstractRpcHandler {

  private var _documentPlugin : IClaimDocumentPlugin
  private var _documentSessionPlugin : IDocumentSessionPlugin
  private var _claimRetrievalPlugin : IClaimRetrievalPlugin
  private var _fileUploadPlugin : IFileUploadPlugin

  @InjectableNode
  @Param("documentPlugin", "Plugin used to deal with claim documents")
  @Param("documentSessionPlugin", "Plugin used to deal with document sessions")
  @Param("claimRetrievalPlugin", "Plugin used to access claim data")
  @Param("fileUploadPlugin", "Plugin used to verify file upload validity")
  construct(documentPlugin : IClaimDocumentPlugin,
  documentSessionPlugin : IDocumentSessionPlugin,
  claimRetrievalPlugin : IClaimRetrievalPlugin,
  fileUploadPlugin : IFileUploadPlugin) {
    this._documentPlugin = documentPlugin
    this._documentSessionPlugin = documentSessionPlugin
    this._claimRetrievalPlugin = claimRetrievalPlugin
    this._fileUploadPlugin = fileUploadPlugin
  }

  /**
   * Used to upload claim documents from the frontend.
   *
   * <dl>
   *  <dt>Calls:</dt>
   *  <dd><code>IDocumentSessionPlugin#isSessionValid(String)</code> - to check if the current document session token is valid in this context.</dd>
   *  <dd><code>IFileUploadPlugin#canUploadContentType(FileItem)</code> - to see if the filetype for the document is allowed by the DMS.</dd>
   *  <dd><code>IClaimRetrievalPlugin#getClaimByNumber(String)</code> - to retrieve the claim.</dd>
   *  <dd><code>IClaimDocumentPlugin#createDocumentMetadata(Bundle, Claim, ClaimDocumentUploadDTO)</code> - to convert DTO into metadata and attach it to the document.</dd>
   *  <dt>Throws:</dt>
   *  <dd><code>AuthorizationException</code> - If session is invalid</dd>
   *  <dd><code>IllegalContentTypeException</code> - If the filetype is not permitted by the DMS</dd>
   *  <dd><code>BadIdentifierException</code> - If the claim number does not exist</dd>
   * </dl>
   *
   * @param documentDto a DTO describing the metadata for the document
   * @param documentFile the document itself
   * @returns A claimDocumentDTO for display in the frontend
   * */
  @JsonRpcMethod
  @ApidocMethodDescription("Used to upload claim documents from the frontend.")
  @ApidocAvailableSince("5.0")
  function upload(documentDto:ClaimDocumentUploadDTO, documentFile: FileItem) : ClaimDocumentDTO {
    try {
      try {
        // validate document upload token
        _documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
      } catch ( ex : JsonRpcSecurityException) {
        throw new AuthorizationException(){:Message = "Unauthorized portal access"}
      }

      if(documentDto.Name!=documentFile.Name){
        throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
      }

      if(documentDto.MimeType!=documentFile.ContentType) {
        throw new IllegalContentTypeException(DisplayKey.get("Document.upload.contentType.error", documentFile.ContentType))
      }

      if (!_fileUploadPlugin.canUploadContentType(documentFile)) {
        throw new IllegalContentTypeException("Cannot upload files of content type: " + documentFile.ContentType)
      }

      var claim = _claimRetrievalPlugin.getClaimByNumber(documentDto.ClaimNumber)

      if (claim == null) {
        throw new BadIdentifierException("Bad claim number " + documentDto.ClaimNumber)
      }

      final var res = Bundle.resolveInTransaction<Document>(\ bundle -> {
        claim = bundle.add(claim)
        final var doc = _documentPlugin.createDocumentMetadata(bundle, claim, documentDto)
        Plugins.get(IDocumentContentSource).addDocument(documentFile.InputStream, doc)
        return doc
      })

      /* Should do this outside the transactions as public ID is not set inside the bundle. */
      return _documentPlugin.getDocumentDetails(res)
    } finally {
      documentFile.InputStream.close()
    }
  }
}
The following codeblock contains the updated file content. Highlighted text indicates updates:
package edge.capabilities.claim.document

uses edge.doc.ApidocAvailableSince
uses edge.doc.ApidocMethodDescription
uses edge.jsonrpc.exception.JsonRpcSecurityException
uses org.apache.commons.fileupload2.core.FileItem
uses org.apache.tika.Tika
uses gw.api.util.Logger
uses edge.jsonrpc.AbstractRpcHandler
uses edge.di.annotations.InjectableNode
uses edge.capabilities.document.IDocumentSessionPlugin
uses edge.jsonrpc.annotation.JsonRpcMethod
uses edge.capabilities.claim.local.IClaimRetrievalPlugin
uses gw.api.webservice.exception.BadIdentifierException
uses edge.PlatformSupport.Bundle
uses gw.plugin.Plugins
uses gw.plugin.document.IDocumentContentSource
uses edge.security.authorization.exception.AuthorizationException
uses edge.capabilities.claim.document.dto.ClaimDocumentUploadDTO
uses edge.capabilities.claim.document.dto.ClaimDocumentDTO
uses edge.security.fileupload.exception.IllegalContentTypeException
uses edge.security.fileupload.exception.FileNameMismatchException
uses gw.api.locale.DisplayKey
uses edge.security.fileupload.IFileUploadPlugin
uses edge.security.fileupload.FileUploadUtil

/**
 * Handler of document uploads.
 * It is needed to work around "security through obscurity" in the authz service and its inability
 * to cope with many different access modes for the same URL. It is also a workaround for a third-party component
 * which is also inflexible and could not accommodate different transports.
 */
class ClaimDocumentUploadHandler extends AbstractRpcHandler {

  private static final var LOG = Logger.forCategory(ClaimDocumentUploadHandler.Type.QName)
  private static final var TIKA = new Tika()
  static final var MIME_PNG = "image/png"
  static final var MIME_JPEG = "image/jpeg"
  static final var MIME_PDF = "application/pdf"

  private var _documentPlugin : IClaimDocumentPlugin
  private var _documentSessionPlugin : IDocumentSessionPlugin
  private var _claimRetrievalPlugin : IClaimRetrievalPlugin
  private var _fileUploadPlugin : IFileUploadPlugin

  @InjectableNode
  @Param("documentPlugin", "Plugin used to deal with claim documents")
  @Param("documentSessionPlugin", "Plugin used to deal with document sessions")
  @Param("claimRetrievalPlugin", "Plugin used to access claim data")
  @Param("fileUploadPlugin", "Plugin used to verify file upload validity")
  construct(documentPlugin : IClaimDocumentPlugin,
  documentSessionPlugin : IDocumentSessionPlugin,
  claimRetrievalPlugin : IClaimRetrievalPlugin,
  fileUploadPlugin : IFileUploadPlugin) {
    this._documentPlugin = documentPlugin
    this._documentSessionPlugin = documentSessionPlugin
    this._claimRetrievalPlugin = claimRetrievalPlugin
    this._fileUploadPlugin = fileUploadPlugin
  }

  /**
   * Used to upload claim documents from the frontend.
   *
   * <dl>
   *  <dt>Calls:</dt>
   *  <dd><code>IDocumentSessionPlugin#isSessionValid(String)</code> - to check if the current document session token is valid in this context.</dd>
   *  <dd><code>IFileUploadPlugin#canUploadContentType(FileItem)</code> - to see if the filetype for the document is allowed by the DMS.</dd>
   *  <dd><code>IClaimRetrievalPlugin#getClaimByNumber(String)</code> - to retrieve the claim.</dd>
   *  <dd><code>IClaimDocumentPlugin#createDocumentMetadata(Bundle, Claim, ClaimDocumentUploadDTO)</code> - to convert DTO into metadata and attach it to the document.</dd>
   *  <dt>Throws:</dt>
   *  <dd><code>AuthorizationException</code> - If session is invalid</dd>
   *  <dd><code>IllegalContentTypeException</code> - If the filetype is not permitted by the DMS</dd>
   *  <dd><code>BadIdentifierException</code> - If the claim number does not exist</dd>
   * </dl>
   *
   * @param documentDto a DTO describing the metadata for the document
   * @param documentFile the document itself
   * @returns A claimDocumentDTO for display in the frontend
   * */
  @JsonRpcMethod
  @ApidocMethodDescription("Used to upload claim documents from the frontend.")
  @ApidocAvailableSince("5.0")
  function upload(documentDto:ClaimDocumentUploadDTO, documentFile: FileItem) : ClaimDocumentDTO {
    try {
      try {
        // validate document upload token
        _documentSessionPlugin.getSessionDocumentId(documentDto.SessionID)
      } catch ( ex : JsonRpcSecurityException) {
        throw new AuthorizationException(){:Message = "Unauthorized portal access"}
      }

      if(documentDto.Name!=documentFile.Name){
        throw new FileNameMismatchException(DisplayKey.get("Document.upload.fileName.error", documentFile.Name))
      }

      FileUploadUtil.assertSafeFileName(documentDto.Name)
      FileUploadUtil.assertSafeFileName(documentFile.Name)

      FileUploadUtil.enforceMaxUploadSize(documentFile)

      var bytes = FileUploadUtil.readAllBytesAndCloseWithLimit(documentFile, FileUploadUtil.getMaxUploadBytes())

      var detectedType = FileUploadUtil.normalizeMimeType(TIKA.detect(bytes), documentFile.Name)

      FileUploadUtil.enforcePdfNameConsistency(documentFile.Name, detectedType, MIME_PDF)

      FileUploadUtil.assertExtensionMatchesType(documentFile.Name, detectedType)

      if (!_fileUploadPlugin.canUploadContentType(detectedType)) {
       throw new IllegalContentTypeException("Cannot upload files of detected content type: " + detectedType)
      }

      FileUploadUtil.validateContent(detectedType, bytes, MIME_PDF, MIME_PNG, MIME_JPEG)

      documentDto.MimeType = detectedType

      var claim = _claimRetrievalPlugin.getClaimByNumber(documentDto.ClaimNumber)
      if (claim == null) {
        throw new BadIdentifierException("Bad claim number " + documentDto.ClaimNumber)
      }

      final var res = Bundle.resolveInTransaction<Document>(\ bundle -> {
        claim = bundle.add(claim)
        final var doc = _documentPlugin.createDocumentMetadata(bundle, claim, documentDto)
        var uploadInputStream = new java.io.ByteArrayInputStream(bytes)
        try {
          Plugins.get(IDocumentContentSource).addDocument(uploadInputStream, doc)
        } finally {
          uploadInputStream.close()
        }
        return doc
      })

      return _documentPlugin.getDocumentDetails(res)
    } finally {
      documentFile.InputStream.close()
    }
  }
}

New Jutro version

InsuranceSuite application affected: N/A

Digital reference application affected: All Digital reference applications

Digital reference applications now use Jutro patch version 10.12.4. This brings enhanced security to the Jutro Design System.

2025.11.1 release

This section lists all of the changes added in patch 1 of the 2025.11 release.

New Jutro version

InsuranceSuite application affected: N/A

Digital reference application affected: All Digital reference applications

Digital reference applications now use Jutro patch version 10.12.3. This brings enhanced security to the Jutro Design System.

2025.11 release

This section lists all of the changes added in the 2025.11 release.

New Jutro version

InsuranceSuite application affected: N/A

Digital reference application affected: All Digital reference applications

Digital reference applications now use Jutro 10.12.2. For more information, see the Jutro 10.12 documentation.

New Node 22 patch version

InsuranceSuite application affected: N/A

Digital reference application affected: All Digital reference applications

The supported Node version for Digital reference applications has been updated to patch version 22.15.1.

New file name utility

InsuranceSuite application affected: PolicyCenter

Digital reference application affected: ProducerEngage

A new file, FileNameMismatchException.gs has been added. If the file is not present in your base configuration, create a new file at gsrc/edge/security/fileupload/exception/FileNameMismatchException.gs and add the following content:

FileNameMismatchException.gs
package edge.security.fileupload.exception

uses edge.exception.ApplicationErrorCode
uses edge.exception.ApplicationException

class FileNameMismatchException extends ApplicationException {

  construct() {
    super(ApplicationErrorCode.GW_SECURITY_ERROR)
  }

  construct(aMessage : String) {
    super(ApplicationErrorCode.GW_SECURITY_ERROR)
    Message = aMessage;
  }

}