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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
Expand All @@ -20,6 +20,7 @@ public class FilterKeyValueAction
/// <summary>
/// 获得/设置 Filter 项字段值
/// </summary>
[JsonConverter(typeof(ObjectWithTypeConverter))]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 issue (security): Using type information from JSON for polymorphic deserialization can be a security risk.

Annotating FieldValue with ObjectWithTypeConverter allows clients to control the $type used during deserialization. Because Type.GetType with assembly-qualified names can resolve arbitrary types, this is unsafe for untrusted input. Use a restricted type resolution strategy (e.g., a whitelist or custom resolver) instead of calling Type.GetType directly on user data.

public object? FieldValue { get; set; }

/// <summary>
Expand Down
117 changes: 11 additions & 106 deletions src/BootstrapBlazor/Converter/JsonQueryPageOptionConverter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
Expand Down Expand Up @@ -104,80 +104,14 @@ public class JsonQueryPageOptionsConverter : JsonConverter<QueryPageOptions>
reader.Read();
ret.IsVirtualScroll = reader.GetBoolean();
}
else if (propertyName == "searches")

else if (propertyName == "filterKeyValueAction")
{
reader.Read();
if (reader.TokenType == JsonTokenType.StartArray)
{
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
{
break;
}
var val = JsonSerializer.Deserialize<SearchFilterAction>(ref reader, options);
if (val != null)
{
ret.Searches.Add(val);
}
}
}
}
else if (propertyName == "customerSearches")
{
reader.Read();
if (reader.TokenType == JsonTokenType.StartArray)
{
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
{
break;
}
var val = JsonSerializer.Deserialize<SearchFilterAction>(ref reader, options);
if (val != null)
{
ret.CustomerSearches.Add(val);
}
}
}
}
else if (propertyName == "advanceSearches")
{
reader.Read();
if (reader.TokenType == JsonTokenType.StartArray)
{
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
{
break;
}
var val = JsonSerializer.Deserialize<SearchFilterAction>(ref reader, options);
if (val != null)
{
ret.AdvanceSearches.Add(val);
}
}
}
}
else if (propertyName == "filters")
{
reader.Read();
if (reader.TokenType == JsonTokenType.StartArray)
var val = JsonSerializer.Deserialize<FilterKeyValueAction>(ref reader, options);
if (val != null)
{
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
{
break;
}
var val = JsonSerializer.Deserialize<SearchFilterAction>(ref reader, options);
if (val != null)
{
ret.Filters.Add(val);
}
}
ret.FilterKeyValueAction = val;
}
}
else if (propertyName == "isFirstQuery")
Expand Down Expand Up @@ -252,41 +186,12 @@ public override void Write(Utf8JsonWriter writer, QueryPageOptions value, JsonSe
{
writer.WriteBoolean("isVirtualScroll", value.IsVirtualScroll);
}
if (value.Searches.Count != 0)
{
writer.WriteStartArray("searches");
foreach (var filter in value.Searches)
{
writer.WriteRawValue(JsonSerializer.Serialize(filter, options));
}
writer.WriteEndArray();
}
if (value.CustomerSearches.Count != 0)
{
writer.WriteStartArray("customerSearches");
foreach (var filter in value.CustomerSearches)
{
writer.WriteRawValue(JsonSerializer.Serialize(filter, options));
}
writer.WriteEndArray();
}
if (value.AdvanceSearches.Count != 0)
{
writer.WriteStartArray("advanceSearches");
foreach (var filter in value.AdvanceSearches)
{
writer.WriteRawValue(JsonSerializer.Serialize(filter, options));
}
writer.WriteEndArray();
}
if (value.Filters.Count != 0)

if (value.Searches.Count != 0||value.CustomerSearches.Count != 0||value.AdvanceSearches.Count != 0|| value.Filters.Count != 0)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Guard against the case where FilterKeyValueAction is already set but filter lists are empty.

Because deserialization sets FilterKeyValueAction directly and leaves Searches / CustomerSearches / AdvanceSearches / Filters empty, this if will skip writing filters when all lists are empty even if FilterKeyValueAction is populated (e.g., after a round-trip). To avoid losing existing filter state on re‑serialization, also include value.FilterKeyValueAction != null in this condition.

{
writer.WriteStartArray("filters");
foreach (var filter in value.Filters)
{
writer.WriteRawValue(JsonSerializer.Serialize(filter, options));
}
writer.WriteEndArray();
writer.WritePropertyName("filterKeyValueAction");
var filterKeyValueAction = value.ToFilter();
writer.WriteRawValue(JsonSerializer.Serialize(filterKeyValueAction, options));
}
if (value.IsFirstQuery)
{
Expand Down
34 changes: 34 additions & 0 deletions src/BootstrapBlazor/Converter/ObjectWithTypeConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone

using System.Text.Json;
using System.Text.Json.Serialization;

namespace BootstrapBlazor.Components;

internal class ObjectWithTypeConverter : JsonConverter<object>
{
public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var doc = JsonDocument.ParseValue(ref reader);

if (!doc.RootElement.TryGetProperty("$type", out var typeProp))
return doc.RootElement.Clone(); // 无类型信息

var type = Type.GetType(typeProp.GetString()!)!;

var valueElement = doc.RootElement.GetProperty("value");
return JsonSerializer.Deserialize(valueElement.GetRawText(), type, options);
Comment on lines +20 to +23
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Handle cases where the specified type cannot be resolved or the value property is missing.

Type.GetType can return null (e.g., type moved, different assembly, trimming), and the null-forgiving operator will hide this until a later NRE. Also, if $type is present but value is missing, GetProperty("value") will throw. Please handle these cases explicitly (e.g., detect and throw a JsonException, fall back to JsonElement, or apply a migration strategy) instead of relying on implicit null/KeyNotFound exceptions.

}

public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteString("$type", value.GetType().AssemblyQualifiedName);
writer.WritePropertyName("value");
JsonSerializer.Serialize(writer, value, value.GetType(), options);
writer.WriteEndObject();
}
}
5 changes: 4 additions & 1 deletion src/BootstrapBlazor/Extensions/QueryPageOptionsExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
Expand All @@ -19,6 +19,9 @@ public static class QueryPageOptionsExtensions
/// <returns></returns>
public static FilterKeyValueAction ToFilter(this QueryPageOptions option)
{
// 后续再更改
if (option.FilterKeyValueAction != null) return option.FilterKeyValueAction;

var filter = new FilterKeyValueAction();

// 处理模糊搜索
Expand Down
13 changes: 12 additions & 1 deletion src/BootstrapBlazor/Options/QueryPageOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
Expand Down Expand Up @@ -41,6 +41,7 @@ public class QueryPageOptions
/// <summary>
/// 获得 搜索条件绑定模型 未设置 <see cref="Table{TItem}.CustomerSearchModel"/> 时为 <see cref="Table{TItem}"/> 泛型模型
/// </summary>
[JsonConverter(typeof(ObjectWithTypeConverter))]
public object? SearchModel { get; set; }

/// <summary>
Expand Down Expand Up @@ -79,6 +80,7 @@ public class QueryPageOptions
/// <summary>
/// 获得 通过列集合中的 <see cref="ITableColumn.Searchable"/> 列与 <see cref="SearchText"/> 拼装 IFilterAction 集合
/// </summary>
[JsonIgnore]
public List<IFilterAction> Searches { get; } = new(20);

/// <summary>
Expand All @@ -91,6 +93,7 @@ public class QueryPageOptions
/// <summary>
/// 获得 <see cref="Table{TItem}.CustomerSearchModel"/> 中过滤条件 <see cref="Table{TItem}.SearchTemplate"/> 模板中的条件请使用 <see cref="AdvanceSearches" />获得
/// </summary>
[JsonIgnore]
public List<IFilterAction> CustomerSearches { get; } = new(20);

/// <summary>
Expand All @@ -103,13 +106,21 @@ public class QueryPageOptions
/// <summary>
/// 获得 <see cref="Table{TItem}.SearchModel"/> 中过滤条件
/// </summary>
[JsonIgnore]
public List<IFilterAction> AdvanceSearches { get; } = new(20);

/// <summary>
/// 获得 过滤条件集合 等同于 <see cref="Table{TItem}.Filters"/> 值
/// </summary>
[JsonIgnore]
public List<IFilterAction> Filters { get; } = new(20);

/// <summary>
/// Gets or sets the action to take when filtering key-value pairs during processing.
/// </summary>
[JsonIgnore]
internal FilterKeyValueAction? FilterKeyValueAction { get; set; }

/// <summary>
/// 获得 是否为首次查询 默认 false
/// </summary>
Expand Down
Loading