{"record":{"id":"5269c10bd415104c","repo":"iflytek/astron-agent","slug":"non-positive-skill-id","errorCode":null,"errorMessage":"non-positive skill id","messagePattern":"non-positive skill id","errorType":"exception","errorClass":"NumberFormatException","httpStatus":null,"severity":"warning","filePath":"console/backend/toolkit/src/main/java/com/iflytek/astron/console/toolkit/service/skill/SkillEnrichmentService.java","lineNumber":70,"sourceCode":"    public void enrichSkillEntries(JSONArray skillArray, String uid, Long spaceId) {\n        if (skillArray == null || skillArray.isEmpty()) {\n            return;\n        }\n        Set<Long> skillIds = new LinkedHashSet<>();\n        for (int i = skillArray.size() - 1; i >= 0; i--) {\n            Object obj = skillArray.get(i);\n            if (!(obj instanceof Map skillObj)) {\n                skillArray.remove(i);\n                continue;\n            }\n            // Runtime metadata is server-derived. Remove every historical/client value before\n            // looking at the id so no invalid or unauthorized entry can retain an attacker URL.\n            SERVER_DERIVED_FIELDS.forEach(skillObj::remove);\n            Object skillIdObj = skillIdentifier(skillObj);\n            try {\n                long skillId = Long.parseLong(String.valueOf(skillIdObj));\n                if (skillId <= 0) {\n                    throw new NumberFormatException(\"non-positive skill id\");\n                }\n                skillIds.add(skillId);\n            } catch (NumberFormatException ex) {\n                log.warn(\"Ignore invalid skill id: {}\", skillIdObj);\n                skillArray.remove(i);\n            }\n        }\n        if (skillIds.isEmpty()) {\n            return;\n        }\n        List<Long> requestedIds = skillIds.stream().sorted().toList();\n        List<SkillImportDto> imports = uid == null\n                ? skillFileService.getSkillImportsByIds(requestedIds)\n                : skillFileService.getSkillImportsByIds(requestedIds, uid, spaceId);\n        Map<Long, SkillImportDto> importMap = Objects.requireNonNullElse(imports, List.<SkillImportDto>of())\n                .stream()\n                .filter(Objects::nonNull)\n                .filter(item -> item.getId() != null)","sourceCodeStart":52,"sourceCodeEnd":88,"githubUrl":"https://github.com/iflytek/astron-agent/blob/5e758547a83371a5a4b29dadf4ac03e8dd527635/console/backend/toolkit/src/main/java/com/iflytek/astron/console/toolkit/service/skill/SkillEnrichmentService.java#L52-L88","documentation":"SkillEnrichmentService.enrichSkillEntries parses each skill entry's identifier as a positive long. If the id is missing, non-numeric, or <= 0, a NumberFormatException(\"non-positive skill id\") is thrown internally and immediately caught: the offending entry is logged as a warning and removed from the array. The exception never escapes the method; it is a control-flow signal used to sanitize the skill list against attacker-controlled URLs.","triggerScenarios":"Calling enrichSkillEntries with a skill object whose `id` field is null, a non-numeric string, or a number <= 0; the entry is silently dropped from the returned list.","commonSituations":"Upstream API returns skill entries with string ids like \"new\" or \"-1\"; manually crafted responses from an untrusted skill source; schema drift where the id field was renamed or became an object.","solutions":["Ensure every skill entry passed to enrichSkillEntries carries a positive numeric id.","Check the log line 'Ignore invalid skill id' to identify which entry was dropped and why.","If entries legitimately lack ids, fetch/assign server-derived ids before enrichment.","If dropping is unexpected, validate the upstream payload shape before calling the service."],"exampleFix":"// before\nentries.add(Map.of(\"id\", \"temp-1\", \"url\", \"https://example.com\"));\nskillEnrichmentService.enrichSkillEntries(entries);\n\n// after\nentries.add(Map.of(\"id\", 42, \"url\", \"https://example.com\"));\nskillEnrichmentService.enrichSkillEntries(entries);","handlingStrategy":"validation","validationCode":"const valid = entries.every(e => { const n = Number(e.id); return Number.isFinite(n) && n > 0; });\nif (!valid) entries = entries.filter(e => Number(e.id) > 0);","typeGuard":"function hasPositiveNumericId(e) { const n = Number(e?.id); return Number.isFinite(n) && n > 0; }","tryCatchPattern":null,"preventionTips":["Validate upstream skill payloads for a positive numeric id before enrichment.","Monitor the 'Ignore invalid skill id' warnings to catch provider schema drift.","Assign server-derived ids to local/unsaved skills before enrichment."],"tags":["input-validation","skill","sanitization"],"backgroundTag":"invalid-identifier-format","analyzedSha":"5e758547a83371a5a4b29dadf4ac03e8dd527635","analyzedAt":"2026-09-12T08:03:51.356Z","contentChangedAt":"2026-09-12T08:03:51.356Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}