{"record":{"id":"cbeb500c5c8f54db","repo":"halo-dev/halo","slug":"unsupported-url-protocol","errorCode":null,"errorMessage":"Unsupported URL protocol: {}","messagePattern":"Unsupported URL protocol: (.+?)","errorType":"http","errorClass":"ServerWebInputException","httpStatus":400,"severity":"warning","filePath":"application/src/main/java/run/halo/app/core/attachment/AttachmentPermalinkMatcher.java","lineNumber":69,"sourceCode":"        return urls.stream().map(url -> createCandidate(url, siteUrl)).toList();\n    }\n\n    private static Candidate createCandidate(String url, URL siteUrl) {\n        if (!StringUtils.hasText(url)) {\n            throw new ServerWebInputException(\"url must not be blank.\");\n        }\n        var value = url.strip();\n\n        URI valueUri;\n        try {\n            valueUri = URI.create(value);\n        } catch (IllegalArgumentException e) {\n            var permalinks = new LinkedHashSet<String>();\n            permalinks.add(value);\n            return new Candidate(url, permalinks);\n        }\n        if (isUnsupportedAbsoluteUri(valueUri)) {\n            throw new ServerWebInputException(\"Unsupported URL protocol: \" + value);\n        }\n\n        var permalinks = new LinkedHashSet<String>();\n        permalinks.add(value);\n        var siteUri = URI.create(siteUrl.toString());\n        if (valueUri.isAbsolute()) {\n            if (sameAuthority(siteUri, valueUri)) {\n                permalinks.add(pathAndQuery(valueUri));\n            }\n        } else {\n            permalinks.add(siteUri.resolve(valueUri).normalize().toString());\n        }\n        return new Candidate(url, permalinks);\n    }\n\n    private static boolean isUnsupportedAbsoluteUri(URI uri) {\n        if (!uri.isAbsolute()) {\n            return false;","sourceCodeStart":51,"sourceCodeEnd":87,"githubUrl":"https://github.com/halo-dev/halo/blob/d2f5165f9c8f055ffcb3fa9c3f4032821a7b68c8/application/src/main/java/run/halo/app/core/attachment/AttachmentPermalinkMatcher.java#L51-L87","documentation":"AttachmentPermalinkMatcher only resolves absolute URIs whose scheme is http or https (SUPPORTED_ABSOLUTE_URI_SCHEMES). createCandidate throws ServerWebInputException (HTTP 400) with the offending value when a candidate is an absolute URI with any other scheme — e.g. ftp://, file://, mailto:, javascript:, data:.","triggerScenarios":"Submitting a permalink candidate like ftp://host/file, file:///etc/passwd, mailto:a@b.com, or any custom-scheme absolute URI to the attachment match endpoint. Relative URLs and http/https absolute URLs are accepted.","commonSituations":"Pasting a file:// link copied from a local file browser; legacy content containing ftp:// downloads; a sanitizer that left a javascript: or data: URL in place; malformed input where a scheme was prepended unintentionally.","solutions":["Use only http:// or https:// absolute URLs, or send relative paths.","Strip or reject non-http(s) schemes on the client before submission.","If matching legacy ftp/file links is truly required, copy the resource to attachment storage and reference its https permalink instead."],"exampleFix":"// before\nurls: [\"ftp://host/archive.zip\"] // 400\n\n// after\nurls: [\"https://host/archive.zip\"]","handlingStrategy":"validation","validationCode":"static final Set<String> OK = Set.of(\"http\", \"https\");\nString scheme;\ntry { scheme = URI.create(url.trim()).getScheme(); }\ncatch (IllegalArgumentException e) { scheme = null; }\nif (scheme != null && !OK.contains(scheme.toLowerCase(Locale.ROOT))) {\n    // reject or convert before calling match()\n}","typeGuard":"static boolean isSupportedUrl(String url) {\n    var v = url == null ? null : url.strip();\n    if (!StringUtils.hasText(v)) return false;\n    URI u;\n    try { u = URI.create(v); } catch (IllegalArgumentException e) { return true; }\n    return !u.isAbsolute() || Set.of(\"http\", \"https\")\n        .contains(u.getScheme().toLowerCase(Locale.ROOT));\n}","tryCatchPattern":null,"preventionTips":["Restrict to http/https absolute URLs or relative paths.","Sanitize content before persisting to avoid ftp/file/javascript URLs reaching the matcher.","Reject non-http(s) schemes at the form/input layer."],"tags":["validation","attachment","permalink","url-scheme","http-400","security"],"backgroundTag":null,"analyzedSha":"d2f5165f9c8f055ffcb3fa9c3f4032821a7b68c8","analyzedAt":"2026-08-14T00:18:38.915Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}