Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d56b062
PageFlowUtil.encodeFormName() and decodeFormName()
labkey-matthewb Mar 19, 2026
7cb411f
Merge remote-tracking branch 'origin/develop' into fb_encodeFormName
labkey-matthewb Mar 21, 2026
9946543
EscapeUtil.getFormFieldName()
labkey-matthewb Mar 21, 2026
21bd355
Merge remote-tracking branch 'origin/develop' into fb_encodeFormName
labkey-matthewb Mar 23, 2026
1a45338
BaseViewAction.getFiles()
labkey-matthewb Mar 23, 2026
e4a743a
Merge remote-tracking branch 'origin/develop' into fb_encodeFormName
labkey-matthewb Mar 23, 2026
734ece4
undo incorrect search/replace
labkey-matthewb Mar 23, 2026
51db816
prefer getProperty() over getRequest().getParameter()
labkey-matthewb Mar 23, 2026
d969731
PageFlowUtil.getFileMap()
labkey-matthewb Mar 24, 2026
7d1a56b
Merge remote-tracking branch 'origin/develop' into fb_encodeFormName
labkey-matthewb Mar 24, 2026
b56f038
Bump @labkey/components
labkey-nicka Mar 25, 2026
bc925a0
WARNING for MultipartRequest methods directly
labkey-matthewb Mar 25, 2026
f8cae4f
Merge remote-tracking branch 'origin/fb_encodeFormName' into fb_encod…
labkey-matthewb Mar 25, 2026
0653561
Merge remote-tracking branch 'origin/develop' into fb_encodeFormName
labkey-matthewb Mar 25, 2026
2a3e7b4
WARNING for MultipartRequest methods directly
labkey-matthewb Mar 25, 2026
6ed42aa
revert unrelated change
labkey-matthewb Mar 25, 2026
d36f745
Missed this overriden version of getColumnByFormFieldName()
labkey-matthewb Mar 25, 2026
c7b22c3
Merge branch 'develop' into fb_encodeFormName
labkey-nicka Mar 25, 2026
4205ffa
Bump @labkey/components
labkey-nicka Mar 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions api/src/org/labkey/api/action/AbstractFileUploadAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -192,19 +192,16 @@
return;
}

HttpServletRequest basicRequest = getViewContext().getRequest();

// Parameter name (String) -> File on disk/original file name Pair
Map<String, Pair<FileLike, String>> savedFiles = new HashMap<>();

if (basicRequest instanceof MultipartHttpServletRequest request)
if (getViewContext().getRequest() instanceof MultipartHttpServletRequest)
{

Iterator<String> nameIterator = request.getFileNames();
while (nameIterator.hasNext())
Map<String, MultipartFile> fileMap = getFileMap();
for (var e : fileMap.entrySet())
{
String formElementName = nameIterator.next();
MultipartFile file = request.getFile(formElementName);
var formElementName = e.getKey();
var file = e.getValue();
String filename = file.getOriginalFilename();

try (InputStream input = file.getInputStream())
Expand Down Expand Up @@ -241,7 +238,7 @@
writer.write(getResponse(form, savedFiles));
writer.flush();
}
catch (UploadException e)

Check warning

Code scanning / CodeQL

Cross-site scripting Medium

Cross-site scripting vulnerability due to a
user-provided value
.
{
error(writer, "Must include the same number of fileName and fileContent parameter values", HttpServletResponse.SC_BAD_REQUEST);
}
Expand Down
46 changes: 10 additions & 36 deletions api/src/org/labkey/api/action/BaseViewAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
Expand Down Expand Up @@ -185,51 +186,26 @@ public static PropertyValues getPropertyValuesForFormBinding(PropertyValues pvs,
return ret;
}

static final String FORM_DATE_ENCODED_PARAM = "formDataEncoded";

/**
* When a double quote is encountered in a multipart/form-data context, it is encoded as %22 using URL-encoding by browsers.
* This process replaces the double quote with its hexadecimal equivalent in a URL-safe format, preventing it from being misinterpreted as the end of a value or a boundary.
* The consequence of such encoding is we can't distinguish '"' from the actual '%22' in parameter name.
* As a workaround, a client-side util `encodeFormDataQuote` is used to convert %22 to %2522 and " to %22 explicitly, while passing in an additional param formDataEncoded=true.
* This class converts those encoded param names back to its decoded form during PropertyValues binding.
* See Issue 52827, 52925 and 52119 for more information.
*/
/// Some characters can be mishandled by the browser in multipart/formdata requests (e.g. doublequote and backslask).
/// We support an encoding from fields to avoid these characters, see {@link PageFlowUtil#encodeFormName} and {@link PageFlowUtil#decodeFormName}.
static public class ViewActionParameterPropertyValues extends ServletRequestParameterPropertyValues
{

public ViewActionParameterPropertyValues(ServletRequest request) {
this(request, null, null);
}

public ViewActionParameterPropertyValues(ServletRequest request, @Nullable String prefix, @Nullable String prefixSeparator)
{
super(request, prefix, prefixSeparator);
if (isFormDataEncoded())
{
for (int i = 0; i < getPropertyValues().length; i++)
{
PropertyValue formDataPropValue = getPropertyValues()[i];
String propValueName = formDataPropValue.getName();
String decoded = PageFlowUtil.decodeQuoteEncodedFormDataKey(propValueName);
if (!propValueName.equals(decoded))
setPropertyValueAt(new PropertyValue(decoded, formDataPropValue.getValue()), i);
}
}
}

private boolean isFormDataEncoded()
{
PropertyValue formDataPropValue = getPropertyValue(FORM_DATE_ENCODED_PARAM);
if (formDataPropValue != null)
for (int i = 0; i < getPropertyValues().length; i++)
{
Object v = formDataPropValue.getValue();
String formDataPropValueStr = v == null ? null : String.valueOf(v);
if (StringUtils.isNotBlank(formDataPropValueStr))
return (Boolean) ConvertUtils.convert(formDataPropValueStr, Boolean.class);
PropertyValue formDataPropValue = getPropertyValues()[i];
String propValueName = formDataPropValue.getName();
String decoded = PageFlowUtil.decodeFormName(propValueName);
if (!propValueName.equals(decoded))
setPropertyValueAt(new PropertyValue(decoded, formDataPropValue.getValue()), i);
}

return false;
}
}

Expand Down Expand Up @@ -725,9 +701,7 @@ public <T> T convertIfNecessary(Object value, Class<T> requiredType, MethodParam
*/
protected Map<String, MultipartFile> getFileMap()
{
if (getViewContext().getRequest() instanceof MultipartHttpServletRequest)
return ((MultipartHttpServletRequest)getViewContext().getRequest()).getFileMap();
return Collections.emptyMap();
return PageFlowUtil.getFileMap(getViewContext().getRequest());
}

protected List<AttachmentFile> getAttachmentFileList()
Expand Down
3 changes: 2 additions & 1 deletion api/src/org/labkey/api/assay/AssayFileWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.labkey.api.query.AbstractQueryUpdateService;
import org.labkey.api.util.FileUtil;
import org.labkey.api.util.NetworkDrive;
import org.labkey.api.util.PageFlowUtil;
import org.labkey.api.view.ViewContext;
import org.labkey.vfs.FileLike;
import org.springframework.web.multipart.MultipartFile;
Expand Down Expand Up @@ -233,7 +234,7 @@ public Map<String, FileLike> savePostedFiles(ContextType context, @NotNull Set<S
Set<String> originalFileNames = new HashSet<>();
if (context.getRequest() instanceof MultipartHttpServletRequest multipartRequest)
{
Iterator<Map.Entry<String, List<MultipartFile>>> iter = multipartRequest.getMultiFileMap().entrySet().iterator();
Iterator<Map.Entry<String, List<MultipartFile>>> iter = PageFlowUtil.getMultiFileMap(context.getRequest()).entrySet().iterator();
Deque<FileLike> overflowFiles = new ArrayDeque<>(); // using a deque for easy removal of single elements
Set<String> unusedParameterNames = new HashSet<>(parameterNames);
while (iter.hasNext())
Expand Down
5 changes: 4 additions & 1 deletion api/src/org/labkey/api/assay/actions/AssayRunUploadForm.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.labkey.api.action.BaseViewAction;
import org.labkey.api.assay.AbstractAssayProvider;
import org.labkey.api.assay.AssayDataCollector;
import org.labkey.api.assay.AssayFileWriter;
Expand Down Expand Up @@ -61,6 +62,7 @@
import org.labkey.api.study.assay.ParticipantVisitResolverType;
import org.labkey.api.util.FileUtil;
import org.labkey.api.util.GUID;
import org.labkey.api.util.PageFlowUtil;
import org.labkey.api.view.ActionURL;
import org.labkey.api.view.NotFoundException;
import org.labkey.api.view.UnauthorizedException;
Expand Down Expand Up @@ -360,10 +362,11 @@ public Map<DomainProperty, FileLike> getAdditionalPostedFiles(List<? extends Dom
File assayDirectory = getAssayDirectory(getContainer(), null);

// Hidden values in form containing previously uploaded files if the previous upload resulted in error
var fileMap = PageFlowUtil.getFileMap(request);
for (String fileParam : filePdNames)
{
DomainProperty domainProperty = fileParameters.get(fileParam);
MultipartFile multiFile = request.getFileMap().get(fileParam);
MultipartFile multiFile = fileMap.get(fileParam);

// If the file is removed from form after error, override hidden file name with an empty file
if (null != multiFile && multiFile.getOriginalFilename().isEmpty())
Expand Down
25 changes: 9 additions & 16 deletions api/src/org/labkey/api/data/TableViewForm.java
Original file line number Diff line number Diff line change
Expand Up @@ -553,24 +553,21 @@ public CaseInsensitiveHashMap<Object> getTypedColumns(boolean includeUntyped)

for (ColumnInfo column : getTable().getColumns())
{
var fieldName = getFormFieldName(column);

if (hasTypedValue(column))
{
values.put(column.getName(), getTypedValue(column));
}
else if (includeUntyped && _stringValues.containsKey(getFormFieldName(column)))
else if (includeUntyped && _stringValues.containsKey(fieldName))
{
values.put(column.getName(), _stringValues.get(getFormFieldName(column)));
values.put(column.getName(), _stringValues.get(fieldName));
}
else if (getRequest() instanceof MultipartHttpServletRequest request)
{
String fieldName = getMultiPartFormFieldName(column);
Object typedValue = _getTypedValues().get(fieldName);

if (typedValue != null)
values.put(column.getName(), typedValue);
else if (File.class.equals(column.getJavaClass()))
if (File.class.equals(column.getJavaClass()))
{
MultipartFile file = request.getFile(fieldName);
MultipartFile file = PageFlowUtil.getFileMap(request).get(fieldName);
if (file != null)
{
// Check if the file was removed
Expand All @@ -587,10 +584,11 @@ else if (File.class.equals(column.getJavaClass()))
ColumnInfo mvColumn = getTable().getColumn(column.getMvColumnName());
if (null != mvColumn)
{
var mvFieldName = getFormFieldName(mvColumn);
if (hasTypedValue(mvColumn))
values.put(mvColumn.getName(), getTypedValue(mvColumn));
else if (includeUntyped && _stringValues.containsKey(getFormFieldName(mvColumn)))
values.put(mvColumn.getName(), _stringValues.get(getFormFieldName(mvColumn)));
else if (includeUntyped && _stringValues.containsKey(mvFieldName))
values.put(mvColumn.getName(), _stringValues.get(mvFieldName));
}
}
}
Expand Down Expand Up @@ -739,11 +737,6 @@ public String getFormFieldName(@NotNull ColumnInfo column)
return column.getName();
}

public String getMultiPartFormFieldName(@NotNull ColumnInfo column)
{
return getFormFieldName(column);
}

@Nullable
public ColumnInfo getColumnByFormFieldName(@NotNull String name)
{
Expand Down
31 changes: 0 additions & 31 deletions api/src/org/labkey/api/dataiterator/DataIteratorUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -148,34 +148,6 @@ public enum MatchType
alias,
jdbcname,
tsvColumn,
multiPartFormData()
{
@Override
public String getMatchedName(@Nullable String name)
{
if (name == null)
return null;
// " is encoded as %22 when content-type is "multipart/form-data" (but is not otherwise encoded so decode() does not work)
return name.replaceAll("\"", "%22");
}

@Override
public boolean updateRowMap(@NotNull ColumnInfo col, Map<String, Object> rowMap)
{
if (col.getName().contains("\"") && File.class.equals(col.getJavaClass()))
{
// Issue 52827: File/attachment fields with special characters
String quoteEncodedName = DataIteratorUtil.MatchType.multiPartFormData.getMatchedName(col.getName());
if (rowMap.containsKey(quoteEncodedName))
{
rowMap.put(col.getName(), rowMap.get(quoteEncodedName));
rowMap.remove(quoteEncodedName);
return true;
}
}
return false;
}
},
low;

public String getMatchedName(@Nullable String name)
Expand Down Expand Up @@ -209,9 +181,6 @@ protected static Map<String,Pair<ColumnInfo,MatchType>> _createTableMap(TableInf

Map<String, Pair<ColumnInfo,MatchType>> targetAliasesMap = new CaseInsensitiveHashMap<>(cols.size()*4);

for (ColumnInfo col : cols)
targetAliasesMap.put(MatchType.multiPartFormData.getMatchedName(col.getName()), new Pair<>(col, MatchType.multiPartFormData));

// should this be under the useImportAliases flag???
for (ColumnInfo col : cols)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import org.labkey.api.data.TableInfo;
import org.labkey.api.data.validator.ColumnValidator;
import org.labkey.api.data.validator.ColumnValidators;
import org.labkey.api.data.validator.RequiredValidator;
import org.labkey.api.exp.PropertyDescriptor;
import org.labkey.api.exp.PropertyType;
import org.labkey.api.exp.api.ExperimentService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ public boolean handlePost(FormType domainIdForm, BindException errors) throws Ex
*/
protected String encodePropertyValues(FormType domainIdForm, String propName) throws IOException
{
return domainIdForm.getRequest().getParameter(propName);
return (String)getProperty(propName);
}

@Override
Expand Down
2 changes: 0 additions & 2 deletions api/src/org/labkey/api/exp/property/DomainUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -1587,8 +1587,6 @@ public static ValidationException validateProperties(@Nullable Domain domain, @N
else
{
altNameMap.put(name, name);
altNameMap.put(DataIteratorUtil.MatchType.multiPartFormData.getMatchedName(name), name);
altNameMap.put(name.replaceAll("%22", "\""), name);
}
}

Expand Down
5 changes: 5 additions & 0 deletions api/src/org/labkey/api/jsp/JspBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,11 @@ public static HtmlString h(URLHelper url)
return h(url == null ? null : url.toString());
}

public static HtmlString hname(String name)
{
return HtmlString.of(PageFlowUtil.encodeFormName(name));
}

// Note: If you have a stream, use LabKeyCollectors.toJsonArray()
public static JSONArray toJsonArray(Collection<?> c)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ else if (_target != null)
}
else if (getViewContext().getRequest() instanceof MultipartHttpServletRequest)
{
Map<String, MultipartFile> files = ((MultipartHttpServletRequest)getViewContext().getRequest()).getFileMap();
Map<String, MultipartFile> files = getFileMap();
MultipartFile multipartfile = null==files ? null : files.get("file");
if (null != multipartfile && multipartfile.getSize() > 0)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
package org.labkey.api.query;

import org.apache.commons.beanutils.ConversionException;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand Down
46 changes: 5 additions & 41 deletions api/src/org/labkey/api/query/QueryUpdateForm.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,52 +76,16 @@ public QueryUpdateForm(@NotNull TableInfo table, @NotNull ViewContext ctx, @Null
@Nullable
public ColumnInfo getColumnByFormFieldName(@NotNull String fieldName)
{
if (!_ignorePrefix && fieldName.length() < PREFIX.length())
return null;

var columnName = _ignorePrefix ? fieldName : fieldName.substring(PREFIX.length());

StringBuilder sb = new StringBuilder(columnName.length());
boolean escaping = false;
for (char c : columnName.toCharArray())
{
if (escaping)
{
sb.append(c);
escaping = false;
}
else if (c == BACKSLASH)
escaping = true;
else
sb.append(c);
}

// Issue 54094: Ensure it works when backslash is the last character
if (escaping)
sb.append(BACKSLASH);

return getTable().getColumn(sb.toString());
String columnName = fieldName;
if (!_ignorePrefix && columnName.startsWith(PREFIX))
columnName = columnName.substring(PREFIX.length());
return getTable().getColumn(columnName);
}

@Override
public String getFormFieldName(@NotNull ColumnInfo column)
{
String columnName = column.getName();
StringBuilder sb = new StringBuilder();
for (char c : columnName.toCharArray())
{
if (SPECIAL_CHARS.indexOf(c) >= 0)
sb.append(BACKSLASH);
sb.append(c);
}

String fieldName = sb.toString();
return _ignorePrefix ? fieldName : PREFIX + fieldName;
}

@Override
public String getMultiPartFormFieldName(@NotNull ColumnInfo column)
{
return DataIteratorUtil.MatchType.multiPartFormData.getMatchedName(getFormFieldName(column));
return _ignorePrefix ? columnName : PREFIX + columnName;
}
}
6 changes: 3 additions & 3 deletions api/src/org/labkey/api/query/excelExportOptions.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,16 @@
%>
<table class="lk-fields-table">
<tr>
<td valign="center"><input type="radio" id="<%=h(xlsxGUID)%>" name="<%=h(checkboxGroupName)%>" checked="checked" /></td>
<td valign="center"><input type="radio" id="<%=h(xlsxGUID)%>" name="<%=hname(checkboxGroupName)%>" checked="checked" /></td>
<td valign="center"><label for="<%=h(xlsxGUID)%>">Excel Workbook (.xlsx)</label> <span style="font-size: smaller">Maximum 1,048,576 rows and 16,384 columns.</span></td>
</tr>
<tr>
<td valign="center"><input type="radio" id="<%=h(xlsGUID)%>" name="<%=h(checkboxGroupName)%>" /></td>
<td valign="center"><input type="radio" id="<%=h(xlsGUID)%>" name="<%=hname(checkboxGroupName)%>" /></td>
<td valign="center"><label for="<%=h(xlsGUID)%>">Excel Old Binary Workbook (.xls)</label> <span style="font-size: smaller">Maximum 65,536 rows and 256 columns.</span></td>
</tr>
<% if (model.getIqyURL() != null) { %>
<tr>
<td valign="center"><input type="radio" id="<%=h(iqyGUID)%>" name="<%=h(checkboxGroupName)%>"/></td>
<td valign="center"><input type="radio" id="<%=h(iqyGUID)%>" name="<%=hname(checkboxGroupName)%>"/></td>
<td valign="center"><label for="<%=h(iqyGUID)%>">Refreshable Web Query (.iqy)</label></td>
</tr>
<% } %>
Expand Down
Loading
Loading