{"record":{"id":"ad799bf458fc0028","repo":"iflytek/astron-agent","slug":"skill-resource-url-is-not-allowed","errorCode":null,"errorMessage":"Skill resource URL is not allowed","messagePattern":"Skill resource URL is not allowed","errorType":"exception","errorClass":"IOException","httpStatus":null,"severity":"error","filePath":"console/backend/hub/src/main/java/com/iflytek/astron/console/hub/service/chat/springai/SkillRuntimeToolService.java","lineNumber":114,"sourceCode":"            }\n            ResponseBody respBody = response.body();\n            return respBody == null\n                    ? \"\"\n                    : decodeText(\n                            readBounded(respBody, maxSandboxResponseBytes, \"Sandbox response\"),\n                            respBody);\n        }\n    }\n\n    /** Download a text resource (SKILL.md or a referenced file) from a presigned URL. */\n    public String downloadText(String url) throws IOException {\n        validateLimit(maxResourceBytes, \"Skill resource\");\n        validateResourceUrl(url);\n        Request request;\n        try {\n            request = new Request.Builder().url(url).get().build();\n        } catch (IllegalArgumentException exception) {\n            throw new IOException(\"Skill resource URL is not allowed\");\n        }\n        OkHttpClient client = httpClient.newBuilder()\n                .callTimeout(Duration.ofSeconds(30))\n                .followRedirects(false)\n                .followSslRedirects(false)\n                .build();\n        try (Response response = client.newCall(request).execute()) {\n            if (!response.isSuccessful()) {\n                throw new IOException(\"Skill resource download failed: HTTP \" + response.code());\n            }\n            ResponseBody body = response.body();\n            if (body == null) {\n                throw new IOException(\"Skill resource download returned empty body\");\n            }\n            return decodeText(readBounded(body, maxResourceBytes, \"Skill resource\"), body);\n        }\n    }\n","sourceCodeStart":96,"sourceCodeEnd":132,"githubUrl":"https://github.com/iflytek/astron-agent/blob/5e758547a83371a5a4b29dadf4ac03e8dd527635/console/backend/hub/src/main/java/com/iflytek/astron/console/hub/service/chat/springai/SkillRuntimeToolService.java#L96-L132","documentation":"downloadText first validates the skill resource URL (validateResourceUrl, origin/allow-list enforcement) and then builds the OkHttp request. If Request.Builder().url(url) throws IllegalArgumentException (malformed URL), it is converted to an IOException \"Skill resource URL is not allowed\". The message deliberately does not distinguish malformed URL from policy-rejected so callers learn nothing about the validation internals.","triggerScenarios":"Calling downloadText (via skill resource read) with a URL that fails validateResourceUrl (foreign origin, non-allow-listed host, disallowed scheme) or with a syntactically invalid URL string that OkHttp cannot parse.","commonSituations":"A skill's resource reference points to an external host not on the allow-list (SSRF protection); a skill manifest contains a relative or malformed URL; the URL was attacker-controlled and got rejected by design; scheme is ftp:// or file:// instead of http(s).","solutions":["Check the URL is absolute, well-formed, and uses the allowed scheme/host required by validateResourceUrl.","Point the skill resource at the approved same-origin host (e.g. the skill repository service) instead of a foreign origin.","Validate/normalize URLs in the skill manifest at publish time so bad URLs never reach runtime.","If a legitimate host is blocked, add it to the configured allow-list — do not bypass the validation."],"exampleFix":"// before\nString text = service.downloadText(\"skills.example.com/file.txt\"); // no scheme -> IllegalArgumentException -> IOException\n// after\nURL u = new URI(\"https://skills.internal.example.com/file.txt\").toURL(); // absolute, allow-listed origin\nif (u.getHost().endsWith(\".internal.example.com\")) {\n    String text = service.downloadText(u.toString());\n}","handlingStrategy":"validation","validationCode":"boolean isAllowedResourceUrl(String url) {\n    try {\n        URI u = new URI(url);\n        String scheme = u.getScheme();\n        String host = u.getHost();\n        return (\"https\".equals(scheme) || \"http\".equals(scheme))\n                && host != null && host.endsWith(\".internal.example.com\");\n    } catch (URISyntaxException e) {\n        return false;\n    }\n}","typeGuard":"boolean isAbsoluteHttpUrl(String url) {\n    try {\n        URI u = new URI(url);\n        return u.isAbsolute() && u.getHost() != null;\n    } catch (URISyntaxException e) {\n        return false;\n    }\n}","tryCatchPattern":"try {\n    String content = service.downloadText(url, limit);\n} catch (IOException e) {\n    if (\"Skill resource URL is not allowed\".equals(e.getMessage())) {\n        // reject the skill resource: malformed or non-allow-listed origin\n    }\n}","preventionTips":["Validate skill resource URLs at skill publish time, not only at runtime.","Keep skill manifests referencing only allow-listed hosts.","Normalize URLs (add scheme, resolve relative paths) before calling downloadText."],"tags":["security","ssrf","url-validation","java","okhttp"],"backgroundTag":"invalid-url","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"}