# IceCMS unauthenticated SQL injection in the WebResource and WebArticle preview-by-num endpoints
. No GitHub release tag exists.
**Class:** CWE-89 Improper Neutralization of Special Elements used in an SQL Command
IceCMS exposes two public preview endpoints that build a database query with an unescaped path variable. `/WebResource/findresourcebynum/{content}/{num}` and `/WebArticle/findarticlesbynum/{content}/{num}` each take a `num` value typed as a `String` and append it to the SQL with MyBatis-Plus `QueryWrapper.last("limit " + num)`. The `last` method concatenates its argument into the final statement with no binding or escaping. Both controller prefixes, `/WebResource` and `/WebArticle`, are absent from the Apache Shiro JWT filter chain in `ShiroConfig`, so both endpoints are reachable without any authentication. An anonymous attacker controls raw SQL after the `LIMIT` keyword in a query against the `resource` and `article` tables. The trust boundary crossed is unauthenticated network input to the application database. The concrete impact is read-oriented SQL injection against the IceCMS database under default configuration.
IceCMS gates authentication with Apache Shiro. The JWT filter is applied only to the path prefixes listed in `ShiroConfig.shiroFilterChainDefinition()`. There is no global `authc` rule, so any route whose prefix is not in the list is served to anonymous callers.
### The authentication chain does not cover the two controllers
`ShiroConfig.java:63-89` builds the filter chain:
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
Map filterMap = new LinkedHashMap<>();
filterMap.put("/articleClass/**", "jwt");
filterMap.put("/ArticleComment/**", "jwt");
filterMap.put("/ResourceClass/**", "jwt");
filterMap.put("/ResourceComment/**", "jwt");
filterMap.put("/Announcements/**", "jwt");
chainDefinition.addPathDefinitions(filterMap);
The prefixes `/WebResource` and `/WebArticle` are not present. No catch-all rule maps the remaining paths to `jwt`, so requests to those prefixes run the controller method with no Shiro subject.
### The resource preview endpoint concatenates the num path variable into SQL
`WebResourceController.java` is mapped at `/WebResource` (`WebResourceController.java:29`). The preview endpoint at `WebResourceController.java:128-134`:
@GetMapping("/findresourcebynum/{content}/{num}")
@PathVariable("content") String content, @PathVariable("num") String num) {
QueryWrapper wrapper = new QueryWrapper<>();
wrapper.like("title", content).last("limit " + num);
return resourceMapper.selectList(wrapper);
`num` is declared as `String`. The value is concatenated into the string `"limit " + num` and handed to `QueryWrapper.last(String)`. In MyBatis-Plus, `last` appends its argument verbatim to the end of the generated SQL. It performs no parameter binding, no quoting, and no validation. The attacker-supplied text lands directly after `LIMIT` in the executed statement.
### The article preview endpoint has the same sink
`WebArticleController.java` is mapped at `/WebArticle` (`WebArticleController.java:36`). The preview endpoint at `WebArticleController.java:169-175`:
@GetMapping("/findarticlesbynum/{content}/{num}")
@PathVariable("content") String content, @PathVariable("num") String num) {
QueryWrapper wrapper = new QueryWrapper<>();
wrapper.like("title", content).last("limit " + num);
return articleMapper.selectList(wrapper);
The structure is identical. `num` is a `String` path variable concatenated into the `LIMIT` clause.
### Sibling endpoints that are not injectable
Several nearby endpoints call `.last("limit " + ...)` but are not injectable because their variable is typed `Integer`, which Spring rejects for non-numeric input. For example `WebArticleController.java:97-106`:
@GetMapping("/getNewArticle/{articleNum}/{check}")
@PathVariable("articleNum") Integer articleNum, @PathVariable("check") String check) {
A request such as `/WebArticle/getNewArticle/abc/new` fails type conversion before the query runs. The two `String`-typed `num` endpoints above are the exploitable ones.
The MyBatis-Plus configuration registers only a pagination plugin. `PageConfiguration.java`:
public PaginationInterceptor paginationInterceptor() {
There is no `SQLInjectionUtils` filter, no `BlockAttackInnerInterceptor`, and no custom SQL sanitizer in the `configuration` package. The datasource URL in `application.yml` does not set `allowMultiQueries`, so stacked statements are not available under default configuration, but in-query injection at the `LIMIT` position is.
participant C as WebResourceController findresourcebynum
participant W as MyBatis-Plus QueryWrapper last
A->>S: GET on WebResource findresourcebynum with crafted num
Note over S: prefix WebResource not in jwt chain, request passes with no subject
S->>C: invoke method with num as raw String
C->>W: build query, append limit plus raw num
W->>DB: execute statement with attacker SQL after LIMIT
DB-->>A: rows shaped by the injected clause
The following requests are illustrative and were not executed against any live deployment. They show the request shape derived from the source.
1. Prerequisite: a reachable IceCMS backend at its default context. No account, token, or cookie is required because the two prefixes are not in the Shiro JWT chain.
2. Baseline request to confirm the endpoint and a normal LIMIT value.
- Request (unexecuted example): `GET /WebResource/findresourcebynum/a/5`
- Expected behavior from source: the query becomes a title LIKE filter on `a` with `LIMIT 5`, returning up to five published resource rows as JSON.
- State change: none, the endpoint is a read.
3. Inject at the LIMIT position. The path variable cannot contain a literal slash or a raw space, so spaces are percent-encoded as `%20`. A union-based probe appended after a valid limit value:
- Request (unexecuted example): `GET /WebResource/findresourcebynum/a/1%20union%20select%201`
- Behavior from source: `num` is concatenated as `limit 1 union select 1`, which changes the executed statement from a simple limited select into a union. The exact column count must match the `resource` select list, so the attacker tunes the number of union columns.
- State change: none, the response body reflects the injected result set rather than the normal rows.
4. Confirm injection with a boolean or time-based expression when union column counts are unknown. A time-based probe:
- Request (unexecuted example): `GET /WebArticle/findarticlesbynum/a/1%20and%20sleep(3)`
- Behavior from source: `num` becomes part of `limit 1 and sleep(3)`, so a measurable delay confirms server-side evaluation of attacker SQL.
- State change: none beyond the induced delay.
5. Extract data once the injection is confirmed, using union columns or blind extraction against application tables. The same two endpoints target the `resource` and `article` tables respectively, and standard MySQL information-schema reads are reachable from the injected clause.
Runtime validation was not performed. No request was sent to any live IceCMS deployment, no repository was cloned or built, and no PoC was executed. The finding is established by static review of the pinned source at commit `afd7a9f0d04a714e7c7e735e566c27f54b5d5201`.
- `/WebResource` controller mapping at `WebResourceController.java:29` and the injectable sink at `WebResourceController.java:128-134` where `num` is a `String` concatenated into `.last("limit " + num)`.
- `/WebArticle` controller mapping at `WebArticleController.java:36` and the injectable sink at `WebArticleController.java:169-175` with the same pattern.
- The Shiro JWT chain at `ShiroConfig.java:63-89` does not include the `/WebResource` or `/WebArticle` prefixes and declares no global `authc` rule, so both endpoints are anonymous.
- No SQL-injection filter or block-attack interceptor is registered in the `configuration` package, only `PaginationInterceptor` in `PageConfiguration.java`.
Not established from source: the exact columns and rows recoverable from a live instance, and whether union or blind extraction is the practical route against a given schema. The datasource URL does not enable `allowMultiQueries`, so stacked-statement execution is not claimed.
An unauthenticated remote attacker controls raw SQL after the `LIMIT` keyword in two queries that run against the IceCMS `resource` and `article` tables. This supports SQL injection with data-disclosure potential, including reading other tables through union selects or blind extraction and reading MySQL information-schema metadata.
- Default self-hosted install: both endpoints are anonymous because their prefixes are not in the Shiro chain, so no precondition beyond network reach applies.
- Hardened deployments that place an external gateway rule in front of `/WebResource` or `/WebArticle`: reach depends on that external rule, which is not part of the application.
Not established: stacked-query execution or data modification, because the reviewed datasource URL does not set `allowMultiQueries`. No remote code execution is claimed. The exact extractable data depends on the live database schema and privileges, which were not inspected.
**Where exactly:** `IceCMS-ment/src/main/java/com/ttice/icewkment/controller/frontend/WebResourceController.java:128-134` and `IceCMS-ment/src/main/java/com/ttice/icewkment/controller/frontend/WebArticleController.java:169-175`.
- @GetMapping("/findresourcebynum/{content}/{num}")
- public List FindresourceByNum(
- @PathVariable("content") String content, @PathVariable("num") String num) {
- QueryWrapper wrapper = new QueryWrapper<>();
- wrapper.like("title", content).last("limit " + num);
- return resourceMapper.selectList(wrapper);
+ @GetMapping("/findresourcebynum/{content}/{num}")
+ public List FindresourceByNum(
+ @PathVariable("content") String content, @PathVariable("num") Integer num) {
+ QueryWrapper wrapper = new QueryWrapper<>();
+ wrapper.like("title", content).last("limit {0}", num);
+ return resourceMapper.selectList(wrapper);
Short term: change `num` from `String` to `Integer` on both endpoints so Spring rejects non-numeric input before the query is built. Apply the same change to `findarticlesbynum`.
Medium term: stop concatenating user input into `QueryWrapper.last`. Use the MyBatis-Plus page API or a bound placeholder form for the limit value, and reject negative or oversized values with an explicit bound.
Defense in depth: register a MyBatis-Plus SQL-inspection interceptor and add input validation for every path variable that reaches a query builder. Review the full controller set for other `.last("limit " + ...)` calls whose variable is a `String`.
Recommended fix: type both `num` path variables as `Integer`, replace the string-concatenated `.last` with a bound limit, and add a server-side numeric bound. This closes the unauthenticated SQL injection at both endpoints.
- No public advisory at time of writing for these two routes. Existing IceCMS CVEs cover different components (UserController CheckVip and ChangeUser, FileUtils uploadFile, squareComment DelectSquareById, square getAllSquare circle, adplanet endpoints, hardcoded JWT key, planet stored XSS) and do not reference `findresourcebynum`, `findarticlesbynum`, or the LIMIT-clause `.last()` concatenation.
- Pinned source, resource sink:
- Pinned source, article sink:
- Pinned source, Shiro JWT chain:
- Pinned source, MyBatis-Plus plugin config:
Reported by hieuPenguinnn, [github.com/hieuPenguinnn](
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
