{"record":{"id":"a55a155a2d7ecc06","repo":"Tencent/APIJSON","slug":"group-value-value-1","errorCode":null,"errorMessage":"@group:value 中 value里面用 , 分割的每一项都必须是1个单词！并且不要有空格！","messagePattern":"@group:value 中 value里面用 , 分割的每一项都必须是1个单词！并且不要有空格！","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java","lineNumber":1662,"sourceCode":"\t\t\t\t//\t\tjoinGroup += (first ? \"\" : \", \") + c;\n\t\t\t\t//\t\tfirst = false;\n\t\t\t\t//\t}\n\t\t\t\t//}\n\t\t\t}\n\t\t}\n\n\n\t\tgroup = StringUtil.trim(group);\n\t\tString[] keys = StringUtil.split(group);\n\t\tif (keys == null || keys.length <= 0) {\n\t\t\treturn StringUtil.isEmpty(joinGroup, true) ? \"\" : (hasPrefix ? \" GROUP BY \" : \"\") + joinGroup;\n\t\t}\n\n\t\tfor (int i = 0; i < keys.length; i++) {\n\t\t\tif (isPrepared()) {\n\t\t\t\t// 不能通过 ? 来代替，因为SQLExecutor<T, M, L> statement.setString后 GROUP BY 'userId' 有单引号，只能返回一条数据，必须去掉单引号才行！\n\t\t\t\tif (StringUtil.isName(keys[i]) == false) {\n\t\t\t\t\tthrow new IllegalArgumentException(\"@group:value 中 value里面用 , 分割的每一项都必须是1个单词！并且不要有空格！\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tkeys[i] = gainKey(keys[i]);\n\t\t}\n\n\t\treturn (hasPrefix ? \" GROUP BY \" : \"\") + StringUtil.concat(StringUtil.get(keys), joinGroup, \", \");\n\t}\n\n\t@Override\n\tpublic String getHavingCombine() {\n\t\treturn havingCombine;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setHavingCombine(String havingCombine) {\n\t\tthis.havingCombine = havingCombine;\n\t\treturn this;\n\t}","sourceCodeStart":1644,"sourceCodeEnd":1680,"githubUrl":"https://github.com/Tencent/APIJSON/blob/5284052872898eddc449a58f629e5c8d588b8e22/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java#L1644-L1680","documentation":"In prepared mode (isPrepared()), AbstractSQLConfig validates every comma-separated item of @group:value with StringUtil.isName — each must be a single identifier word. GROUP BY items cannot be bound as ? parameters, so malformed items would otherwise allow SQL injection.","triggerScenarios":"GET request with \"@group\": \"count(*)\", \"@group\": \"user id\" (space), \"@group\": \"userId;drop\" or any item with quotes/dashes/spaces instead of a bare column name.","commonSituations":"Frontend passes an aggregated expression into @group instead of @column; users paste SQL fragments; column aliases with hyphens or dots.","solutions":["Use only bare column names: \"@group\": \"userId,date\"","Put aggregate functions in @column (e.g. \"count(*):count\") and group by plain columns only","For expressions that cannot be a plain name, pre-define them in backend RAW_MAP and reference via @raw"],"exampleFix":"// before\n{\"@column\": \"count(*)\", \"@group\": \"count(*)\"}\n// after\n{\"@column\": \"count(*):count\", \"@group\": \"userId\"}","handlingStrategy":"validation","validationCode":"const isName = s => /^[A-Za-z_][A-Za-z0-9_]*$/.test(s);\nfor (const item of String(obj['@group'] ?? '').split(',')) {\n  if (item && !isName(item.trim())) throw new Error(`@group item '${item}' must be a bare column name`);\n}","typeGuard":"const isGroupItemValid = s => /^[A-Za-z_][A-Za-z0-9_]*$/.test(s);","tryCatchPattern":"try { await api.get(req); } catch (e) { if (e.message.includes('@group')) sanitizeListField(req, '@group'); else throw e; }","preventionTips":["Generate @group only from a column-name picker, never free text","Keep aggregates in @column with aliases, not in @group"],"tags":["apijson","group","sql-injection","prepared-statement"],"backgroundTag":null,"analyzedSha":"5284052872898eddc449a58f629e5c8d588b8e22","analyzedAt":"2026-08-14T15:15:29.577Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}