A critical null pointer dereference vulnerability has been discovered in the SurveyKing application. Projects with null answerSetting configuration are persistently stored in the database (caused by Lombok @Builder.Default bug with MapStruct/Jackson), causing immediate and complete admin panel crash whenever ANY administrator simply views the project list page.
CRITICAL CLARIFICATION:
- PREREQUISITE: Someone creates an empty project via
POST /api/project/createwithpages: [] - THEN: Database stores corrupted
answerSetting: null - THEN: Admin panel crashes when ANY administrator views project list
- NO answer submission required - crash triggers on project list page load
- NO user attack needed - happens during normal admin navigation
- Affects ALL administrators - anyone accessing
/admin/projectcrashes - Persistent DoS - remains broken until database manually fixed
The vulnerability manifests as:
- Backend:
NullPointerExceptionatSurveyServiceImpl.validateProject()when loading project list - Frontend:
TypeError: Cannot read properties of undefined (reading 'status')causing React component crash - Result: Complete admin panel lockout for ALL users
CRITICAL FINDING: This is NOT an on-demand exploit - it is a persistent code defect. Any project created with empty pages: [] array triggers the bug, permanently breaking the admin panel for ALL administrators.
ROOT CAUSE: Lombok @Builder.Default only initializes answerSetting when using .builder(), NOT when using new ProjectSetting(). MapStruct mapper creates instances via constructor, leaving answerSetting = null.
The SurveyServiceImpl.validateProject() method attempts to access nested properties of answerSetting without first validating that the object is not null. When a project has a null answerSetting configuration stored in the database (caused by Lombok @Builder.Default not working with MapStruct/Jackson constructors), the admin panel crashes immediately upon loading the project list page.
CRITICAL: This vulnerability triggers automatically when administrators view the project list - it does NOT require submitting an answer. The frontend React application crashes with TypeError as soon as it receives the corrupted project data from the backend.
File: server/rdbms/src/main/java/cn/surveyking/server/impl/SurveyServiceImpl.java
Lines: 490, 501, 508
Method: validateProject(ProjectView project)
public void validateProject(ProjectView project) {
if (project == null) {
throw new ErrorCodeException(ErrorCode.ProjectNotFound);
}
ProjectSetting setting = project.getSetting();
String projectId = project.getId();
if (setting.getStatus() == 0) {
throw new ErrorCodeException(ErrorCode.SurveySuspend);
}
// ❌ VULNERABILITY Line 490: No null check for getAnswerSetting()
Long maxAnswers = setting.getAnswerSetting().getMaxAnswers();
if (maxAnswers != null) {
AnswerQuery answerQuery = new AnswerQuery();
answerQuery.setProjectId(project.getId());
long totalAnswers = answerService.count(answerQuery);
if (totalAnswers >= maxAnswers) {
throw new ErrorCodeException(ErrorCode.ExceededMaxAnswers);
}
}
// ❌ VULNERABILITY Line 501: Second null pointer access
Long endTime = setting.getAnswerSetting().getEndTime();
if (endTime != null) {
if (new Date().getTime() > endTime) {
throw new ErrorCodeException(ErrorCode.ExceededEndTime);
}
}
// ❌ VULNERABILITY Line 508: Third null pointer access
if (setting.getAnswerSetting().getLoginLimit() != null
&& Boolean.TRUE.equals(setting.getAnswerSetting().getLoginRequired())) {
// Login validation logic...
}
}Step 1 - Data Corruption (Prerequisite):
- User/Admin creates a project via
POST /api/project/createwithpages: [] - MapStruct creates
ProjectSettingusingnew ProjectSetting()(no-arg constructor) - Lombok
@Builder.Defaultis ignored →answerSetting = null - Database stores corrupted data:
{"answerSetting": null}
Step 2 - Crash Trigger (Automatic):
- ANY administrator visits
/admin/projectpage - Backend calls
ProjectServiceImpl.listProject()→validateProject() validateProject()tries to accesssetting.getAnswerSetting().getMaxAnswers()NullPointerExceptionthrown
Step 3 - Frontend Crash (Cascading Failure):
- Backend returns 500 error with corrupted response
- Frontend React component receives undefined data
TypeError: Cannot read properties of undefined (reading 'status')- Admin panel completely unusable
- Missing Null Validation: Code assumes
answerSettingis always non-null - Lombok @Builder.Default Bug: Only works with
.builder(), notnew ProjectSetting() - MapStruct Uses Constructor: Creates objects via
new ProjectSetting(), leaving fields null - Database Stores Corrupted Data:
{"answerSetting": null}persisted to database - Frontend Crash on Read: React component cannot handle null answerSetting
- No Database Constraints: No CHECK constraint prevents null values
| Impact Category | Severity | Description |
|---|---|---|
| Availability | Critical | Complete admin panel crash |
| Confidentiality | Low | Stack traces may leak internal implementation details via error logs |
| Integrity | None | No data corruption occurs (but database contains corrupted null values) |
| Authentication | None | All authenticated administrators affected - no specific attack required |
POST /api/project/save HTTP/1.1
Host: target.com
Content-Type: application/json
Authorization: Bearer <admin-token>
{
"name": "Empty Survey PoC",
"survey": {
"id": "empty-poc",
"title": "Empty Survey",
"pages": []
}
}
2025-10-23 02:23:59.634 [XNIO-1 task-5] ERROR c.s.s.c.m.a.GlobalExceptionHandler - handleInternalServerError /api/public/saveAnswer
java.lang.NullPointerException: null
at cn.surveyking.server.impl.SurveyServiceImpl.validateProject(SurveyServiceImpl.java:489)
at cn.surveyking.server.impl.SurveyServiceImpl.validateAndGetLatestAnswer(SurveyServiceImpl.java:619)
at cn.surveyking.server.impl.SurveyServiceImpl.saveAnswer(SurveyServiceImpl.java:253)
at cn.surveyking.server.impl.SurveyServiceImpl$FastClassBySpringCGLIB$da227c3e.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:793)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708)
at cn.surveyking.server.impl.SurveyServiceImpl$EnhancerBySpringCGLIB$c3b46f34.saveAnswer(<generated>)
at cn.surveyking.server.api.SurveyApi.saveAnswer(SurveyApi.java:69)
TypeError: Cannot read properties of undefined (reading 'status')
at Za (p__Project.4583a538.async.js:7:25760)
at renderItem (p__Project.4583a538.async.js:7:12768)
at Te (1600.97b9988e.async.js:1:17772)
at 1600.97b9988e.async.js:1:19305
at Array.map (<anonymous>)
at ue (1600.97b9988e.async.js:1:19280)
at tn (umi.02f6b5e7.js:314:54807)
at Vf (umi.02f6b5e7.js:318:9212)
at ul (umi.02f6b5e7.js:318:991)
at Kd (umi.02f6b5e7.js:318:919)Remove @NoArgsConstructor to force Builder usage:
// File: ProjectSetting.java
@Data
// @NoArgsConstructor ❌ REMOVE THIS - causes the bug
@AllArgsConstructor
@Builder
public class ProjectSetting {
@Builder.Default
private Integer status = 0;
private ProjectModeEnum mode;
@Builder.Default
AnswerSetting answerSetting = new AnswerSetting();
@Builder.Default
SubmittedSetting submittedSetting = new SubmittedSetting();
@Builder.Default
ExamSetting examSetting = new ExamSetting();
// ✅ ADD: Custom no-arg constructor with defaults
public ProjectSetting() {
this.status = 0;
this.answerSetting = new AnswerSetting();
this.submittedSetting = new SubmittedSetting();
this.examSetting = new ExamSetting();
}
}Why this works:
- Lombok won't generate broken no-arg constructor
- Custom constructor properly initializes all fields
- MapStruct and Jackson will use the custom constructor
- Builder pattern still works correctly