修复越权删除、畸形链接 500 及两处 NPE
修复上一提交的测试所发现的缺陷。
1) 越权删除(安全,最严重):deleteGalleryByGid 的授权条件是
`!(collector.isEmpty() || collector.size()==1 && ...)`。当画廊无任何收藏时
collector.isEmpty() 使整个条件短路放行,下载者身份完全未校验,
任意有效授权码用户都能删除他人任务记录(无收藏正是最常见情形)。
改为与提示文案一致的判定:被别人收藏 或 请求者不是下载人 即拒绝。
2) 被拒请求仍删除节点文件且对外显示成功:remoteService.deleteGallery 原先位于
授权判断之外,即使拒绝也会向节点下发删除指令;且 switch 中 case 0 的
response.success() 会覆盖先前的 failure。现改为授权失败即提前返回。
3) 畸形链接导致 500:link.split("/")[4] 段数不足抛 ArrayIndexOutOfBoundsException,
而只捕获 NumberFormatException;项目无 @ControllerAdvice,异常穿透为 500。
抽出 parseGidFromLink() 统一把 null/段数不足/非数字转成业务失败。
4) Response.isSuccess()/getData()/get() 在键缺失时抛 NPE,改为安全返回
(isSuccess 视未设置为失败)。deleteGalleryByGid 也显式处理节点无响应(-1),
原先该分支不设置 result,末尾 response.get("result") 会 NPE。
5) getWeekUsedAmount() 在配置行缺失/值非法时给默认 0,不再 NPE。
验证:新增/改写回归用例后 143 项测试全过;并用真实数据做了新旧对照实测——
取 downloader=4 且无收藏的任务,以 id=25 用户的授权码删除:
旧 jar:数据库记录被删(count 0),随后 500;
新 jar:记录保留(count 1),返回「你不是下载人」并被拒。
覆盖率的 GalleryManageService 由 40.1% 升至 45.8%。
This commit is contained in:
@@ -60,14 +60,20 @@ public class GalleryManageService {
|
||||
User user = userMapper.selectUserByAuthCode(AuthCode);
|
||||
// return Response._failure("调试中,请勿提交任务");
|
||||
|
||||
int gid;
|
||||
try {
|
||||
gid = Integer.parseInt(link.split("/")[4]);
|
||||
} catch (NumberFormatException e) {
|
||||
if (user == null) {
|
||||
response.failure("授权码无效");
|
||||
return response.toJSONString();
|
||||
}
|
||||
|
||||
// 段数不足会先抛 ArrayIndexOutOfBoundsException,非数字段抛 NumberFormatException;
|
||||
// 只捕后者会让畸形链接穿透为 500(本项目无 @ControllerAdvice)。
|
||||
Integer parsedGid = parseGidFromLink(link);
|
||||
if (parsedGid == null) {
|
||||
response.failure("链接错误");
|
||||
pushService.taskCreateReport(user.getUsername(), "未知任务", response);
|
||||
return response.toJSONString();
|
||||
}
|
||||
int gid = parsedGid;
|
||||
String taskName = "任务 [" + gid + "]";
|
||||
|
||||
if (remoteService.isDead()) {
|
||||
@@ -126,6 +132,23 @@ public class GalleryManageService {
|
||||
return response.toJSONString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从任务链接中安全提取 gid。任何畸形输入(null、段数不足、非数字)都返回 null,
|
||||
* 由调用方转成业务失败,避免异常穿透为 500。
|
||||
*/
|
||||
static Integer parseGidFromLink(String link) {
|
||||
if (link == null)
|
||||
return null;
|
||||
String[] segments = link.split("/");
|
||||
if (segments.length <= 4)
|
||||
return null;
|
||||
try {
|
||||
return Integer.parseInt(segments[4]);
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试重新连接
|
||||
* @return 重连结果
|
||||
@@ -284,21 +307,35 @@ public class GalleryManageService {
|
||||
response.failure("删除失败,该图片不存在");
|
||||
return response.toJSONString();
|
||||
}
|
||||
if (user == null) {
|
||||
response.failure("删除失败,授权码无效");
|
||||
return response.toJSONString();
|
||||
}
|
||||
|
||||
ArrayList<Integer> collector = collectMapper.selectCollectorByGid(gallery.getGid());
|
||||
if (!(collector.isEmpty() || collector.size() == 1 && collector.getFirst().equals(user.getId()) //判断收藏
|
||||
&& gallery.getDownloader() == user.getId())) //判断下载
|
||||
// 拒绝条件与提示文案一致:被别人收藏,或者请求者不是下载人。
|
||||
// 注意不能写成 collector.isEmpty() || ...:那样在「无任何收藏」时会短路放行,
|
||||
// 从而完全跳过下载者校验,导致任何有效授权码都能删除他人任务。
|
||||
boolean collectedByOthers = collector.stream().anyMatch(id -> id != user.getId());
|
||||
boolean isDownloader = gallery.getDownloader() == user.getId();
|
||||
if (collectedByOthers || !isDownloader) {
|
||||
response.failure("删除失败,该图片已被别人收藏或你不是下载人");
|
||||
else {
|
||||
log.info("删除图片{}", gallery.getName());
|
||||
galleryMapper.deleteGalleryByGid(gallery.getGid()); //删除图片记录
|
||||
log.info("拒绝删除 gid={}:collectedByOthers={} isDownloader={}", gid, collectedByOthers, isDownloader);
|
||||
return response.toJSONString();
|
||||
}
|
||||
|
||||
// 通过授权后才落库并通知节点,避免被拒请求仍删除节点文件。
|
||||
log.info("删除图片{}", gallery.getName());
|
||||
galleryMapper.deleteGalleryByGid(gallery.getGid());
|
||||
switch (remoteService.deleteGallery(gallery)) {
|
||||
case ErrorCode.IO_ERROR -> response.failure("图片:" + gallery.getName() + "删除失败,IO错误");
|
||||
case ErrorCode.FILE_NOT_FOUND -> response.failure("图片:" + gallery.getName() + "删除失败,文件不存在");
|
||||
case 0 -> response.success();
|
||||
// 节点无响应/超时会返回 -1 等非枚举值;必须显式判失败,
|
||||
// 否则 result 保持未设置,末尾的 response.get("result") 会抛 NPE。
|
||||
default -> response.failure("图片:" + gallery.getName() + "删除失败,节点无响应");
|
||||
}
|
||||
if (response.get("result").equals("failure"))
|
||||
if (!response.isSuccess())
|
||||
log.info(response.getData());
|
||||
return response.toJSONString();
|
||||
}
|
||||
@@ -313,8 +350,20 @@ public class GalleryManageService {
|
||||
CustomConfiguration lastResetAmountTime = configurationMapper.selectConfiguration(CustomConfiguration.LAST_RESET_AMOUNT_TIME);
|
||||
Map<String, String> data = new HashMap<>();
|
||||
|
||||
data.put("weekUsedAmount", CustomUtil.fileSizeToString(Long.parseLong(weekUsedAmount.getValue())));
|
||||
data.put("lastResetAmountTime", lastResetAmountTime.getValue());
|
||||
// 配置行缺失时给出默认值,避免 NPE 让用量接口整体不可用。
|
||||
String usedValue = weekUsedAmount == null || weekUsedAmount.getValue() == null
|
||||
? "0" : weekUsedAmount.getValue();
|
||||
long used;
|
||||
try {
|
||||
used = Long.parseLong(usedValue);
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("每周用量配置值非法,按 0 处理: {}", usedValue);
|
||||
used = 0L;
|
||||
}
|
||||
data.put("weekUsedAmount", CustomUtil.fileSizeToString(used));
|
||||
data.put("lastResetAmountTime",
|
||||
lastResetAmountTime == null || lastResetAmountTime.getValue() == null
|
||||
? "" : lastResetAmountTime.getValue());
|
||||
|
||||
response.success(new ObjectMapper().valueToTree(data).toString());
|
||||
return response.toJSONString();
|
||||
|
||||
@@ -19,7 +19,8 @@ public class Response {
|
||||
}
|
||||
|
||||
public String get(String key){
|
||||
return result.get(key).asText();
|
||||
JsonNode node = result.get(key);
|
||||
return node == null ? null : node.asText();
|
||||
}
|
||||
|
||||
public void setData(String data){
|
||||
@@ -57,7 +58,8 @@ public class Response {
|
||||
}
|
||||
|
||||
public String getData(){
|
||||
return result.get("data").asText();
|
||||
JsonNode node = result.get("data");
|
||||
return node == null ? null : node.asText();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,8 +71,10 @@ public class Response {
|
||||
return getData();
|
||||
}
|
||||
|
||||
/** 未设置 result 键时视为失败,而不是抛 NPE。 */
|
||||
public boolean isSuccess(){
|
||||
return result.get("result").asText().equals("success");
|
||||
JsonNode node = result.get("result");
|
||||
return node != null && "success".equals(node.asText());
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user