添付ファイルの拡張子制限
本体の標準機能ではありません
項目ごとの拡張子の許可・拒否は 1.5.8.1 にはありません。このページの後半は改修する場合の設計メモです。
現行の拡張子チェック(1.5.8.1)
設定は Form.json の 1 か所だけ
拡張子の拒否リストはサーバー全体で 1 つで、App_Data/Parameters/Form.json の AttachmentExcludedExtensions にあります(Form.json、Form.cs)。
{
"Enabled": false,
"AttachmentExcludedExtensions": [
".exe", ".dll", ".com", ".scr", ".pif", ".msi", ".msp", ".bat", ".cmd", ".ps1",
".vbs", ".vbe", ".js", ".jse", ".wsf", ".wsh", ".aspx", ".asp", ".php", ".php3",
".php4", ".php5", ".phtml", ".jsp", ".jspx", ".cfm", ".cfc", ".hta", ".htaccess",
".htpasswd", ".config", ".cer", ".sh", ".bash", ".csh", ".ksh", ".pl", ".py",
".rb", ".jar", ".war"
]
}実際のファイルは 1 行に 1 つずつ書かれています。既定で 41 個です。
判定のしかた
判定は BinaryValidators.IsAllowedExtension() です(BinaryValidators.cs#L606-L631)。
- ファイル名の最初の
.から後ろを.で区切り、各部分を拒否リストと照合します。report.exe.pdfもmalware.txt.exeも.exeを含むので拒否されます。.を含まないファイル名は通ります。 - リストは
HashSet<string>で、JSON から読み込むと既定の比較(大文字小文字を区別する)になります。照合の前に小文字にしていないので、.EXEのように大文字で書かれた拡張子はリストの.exeと一致しません。 - 拒否リスト方式だけで、「この拡張子だけ許可する」設定はありません。
掛かるのは公開フォームからのアップロードだけ
IsAllowedExtension() を呼ぶのは OnValidatingFormUpload() だけで、その先頭が if (!context.IsForm) return Error.Types.None; です(BinaryValidators.cs#L636-L689)。
- 公開フォーム(フォーム機能)からのアップロードでは、ファイル名の検査(
IsValidFileName())と拡張子の検査が掛かり、引っかかるとInvalidRequestになります。 - ログインしたユーザーの編集画面や API からのアップロードでは、拡張子は検査しません。
- 公開フォームは
Form.jsonのEnabled(既定false)か、テナントの契約設定の拡張Formで有効になる機能です(SiteUtilities.cs#L20295)。どちらも無効な既定の構成では、このリストは使われていません。
添付ファイル項目の既存の設定
添付ファイル項目の詳細設定(SiteUtilities.cs#L8864-L8952)にあるのは次の項目で、拡張子に関するものはありません。
| 設定 | controlId | 種類 |
|---|---|---|
| 添付ファイルの削除を許可 | AllowDeleteAttachments | チェックボックス |
| 既存の履歴を削除しない | NotDeleteExistHistory | チェックボックス |
| 保存先 | BinaryStorageProvider | ドロップダウン(BinaryStorage.json の UseStorageSelect が有効なときだけ) |
| 同名ファイルを上書き | OverwriteSameFileName | チェックボックス |
| 件数の上限 | LimitQuantity | スピナー |
| 1 ファイルのサイズ上限(MB) | LimitSize | スピナー |
| 合計サイズの上限(MB) | LimitTotalSize | スピナー |
| ローカル保存時の 1 ファイルの上限 | LocalFolderLimitSize | スピナー |
| ローカル保存時の合計の上限 | LocalFolderLimitTotalSize | スピナー |
改修案:項目ごとに許可・拒否する拡張子を設定する
判定の順序
サーバー全体の拒否リストを先に見て、次に項目の拒否リスト、最後に項目の許可リスト(設定があるときだけ)を見ます。
図を読み込み中…
| 設定状態 | 動作 |
|---|---|
| どちらも未設定 | サーバー全体の拒否リストだけ(現行と同じ) |
| 許可リストだけ | リストにある拡張子だけアップロードできる |
| 拒否リストだけ | リストにある拡張子はアップロードできない |
| 両方 | 拒否リストを先に見て、残りを許可リストで絞る。両方に同じ拡張子があれば拒否 |
改修するファイル
| ファイル | 内容 |
|---|---|
Libraries/Settings/Column.cs | HashSet<string> AllowedExtensions・ExcludedExtensions を追加(既存の AllowDeleteAttachments などの並び。Column.cs#L139) |
Libraries/Settings/SiteSettings.cs | 保存用の差分を作る処理(SiteSettings.cs#L1785-L1788 の AllowDeleteAttachments と同じ形)と、画面から来た値を項目に反映する SetColumnProperty の switch(SiteSettings.cs#L4498) |
Models/Sites/SiteUtilities.cs | 添付ファイル項目の詳細設定に、許可・拒否の入力欄を追加 |
Models/Binaries/BinaryValidators.cs | 項目を受け取る判定を追加 |
Libraries/HtmlParts/HtmlControls.cs | ファイル選択の accept 属性 |
| フロントエンドのスクリプト | 入力欄から一覧に拡張子を足す処理 |
Models/Sites/ApiSiteSettings/ColumnApiSettingModel.cs | サイト設定 API 用のモデルに同じプロパティ |
App_Data/Displays/ | 「許可する拡張子」(AllowedExtensions)・「許可しない拡張子」(ExcludedExtensions)の表示文字列 |
Column.cs は CodeDefiner の生成対象ではないので、直接編集して構いません。
入力欄:バスケットを流用する
拡張子の入力には、既存のバスケット(入力した値がタグ状に並び、× で個別に消せる UI)を流用できます。サーバー側は HtmlFields.FieldBasket()(HtmlFields.cs#L2201)、画面側は $p.addBasket()(basket.js)と × クリックで要素を消す basketevents.js です。
.FieldBasket(
controlId: "AllowedExtensions",
fieldCss: "field-wide",
controlCss: "control-basket cf",
labelAction: () => hb
.Text(text: Displays.AllowedExtensions(context: context)))
.FieldTextBox(
textType: HtmlTypes.TextTypes.Normal,
controlId: "AllowedExtensionsInput",
fieldCss: "field-wide",
labelText: string.Empty,
text: string.Empty,
placeholder: ".pdf")
// ExcludedExtensions も同じ形で追加する入力欄から一覧に足すスクリプトの例です。先頭の . を補い、小文字にし、重複を除きます。
$p.addExtensionToBasket = function (inputId, basketId) {
var $input = $('#' + inputId);
var extension = $input.val().trim().toLowerCase();
if (!extension.startsWith('.')) {
extension = '.' + extension;
}
var exists = false;
$('#' + basketId + ' li').each(function () {
if ($(this).find('span:first').text() === extension) {
exists = true;
return false;
}
});
if (!exists && extension.length > 1) {
$p.addBasket($('#' + basketId), extension, extension);
}
$input.val('');
};判定の追加
既存の IsAllowedExtension(fileName) を先に呼び、そのあと項目の設定を見るオーバーロードを足します。項目側の照合は小文字にそろえて行います。
public static bool IsAllowedExtension(string fileName, Column column)
{
if (!IsAllowedExtension(fileName)) return false;
if (column == null) return true;
var firstDotIndex = fileName.IndexOf('.');
if (firstDotIndex < 0) return true;
foreach (var part in fileName[firstDotIndex..].Split('.'))
{
if (string.IsNullOrEmpty(part)) continue;
var extension = "." + part.ToLower();
if (column.ExcludedExtensions?.Contains(extension) == true) return false;
if (column.AllowedExtensions?.Any() == true
&& !column.AllowedExtensions.Contains(extension)) return false;
}
return true;
}- 拡張子のないファイル名は、許可リストがあってもこの判定を通ります。
- 複合拡張子の各部分に許可リストを当てるので、許可リストが
.pdfだけだとreport.v2.pdfのような名前(.v2が許可リストにない)も拒否されます。最後の拡張子だけを見る方式にするかは要件次第です。 - 現行の呼び出し元
OnValidatingFormUpload()はcontext.IsFormでないと何もしないので、画面や API からのアップロードにも項目の設定を効かせるなら、呼び出し位置をUploadFile()のOnUploading()の近く(項目columnを持っている所)に移すか、別に呼びます。API のアップロード(Api/BinariesController)も同様です。
accept 属性(補助)
添付ファイル項目のファイル選択は、1.5.8.1 では accept 属性なしで描画しています(HtmlControls.cs#L1053-L1057)。許可リストがあるときに付けると、ファイル選択ダイアログで該当する種類だけが表示されます。
.Input(attributes: new HtmlAttributes()
.Id(columnName + ".input")
.Class("hidden")
.Type("file")
.Multiple(true)
.Accept(column?.AllowedExtensions?.Any() == true
? string.Join(",", column.AllowedExtensions)
: null))accept はブラウザへのヒントで、「すべてのファイル」を選べば回避できます。サーバー側の判定は必ず残します。