{"record":{"id":"8468be7f34486b5d","repo":"DIYgod/RSSHub","slug":"invalid-url-property-prop","errorCode":null,"errorMessage":"Invalid URL property: ${prop}","messagePattern":"Invalid URL property: (.+?)","errorType":"exception","errorClass":null,"httpStatus":503,"severity":"warning","filePath":"lib/middleware/anti-hotlink.ts","lineNumber":97,"sourceCode":"        replaceUrls($, '*[data-rsshub-image=\"href\"]', image_hotlink_template, 'href');\n    }\n    if (multimedia_hotlink_template) {\n        replaceUrls($, 'video, video > source, audio, audio > source', multimedia_hotlink_template);\n        if (!image_hotlink_template) {\n            replaceUrls($, 'video[poster]', multimedia_hotlink_template, 'poster');\n        }\n    }\n    return $.html();\n};\n\nconst validateTemplate = (template?: string) => {\n    if (!template) {\n        return;\n    }\n    for (const match of template.matchAll(templateRegex)) {\n        const prop = match[1].endsWith('_ue') ? match[1].slice(0, -3) : match[1];\n        if (!allowedUrlProperties.has(prop)) {\n            throw new Error(`Invalid URL property: ${prop}`);\n        }\n    }\n};\n\nconst middleware: MiddlewareHandler = async (ctx, next) => {\n    await next();\n\n    let imageHotlinkTemplate: string | undefined;\n    let multimediaHotlinkTemplate: string | undefined;\n\n    // Read params if enabled\n    if (config.feature.allow_user_hotlink_template) {\n        // By default, the config turns these features off. Set corresponding config to\n        // true to turn this feature on.\n        // A risk is that the media URLs will be replaced by user-supplied templates,\n        // so a user could literally take the control of \"where are the media from\",\n        // but only in their personal-use feed URL.\n        multimediaHotlinkTemplate = ctx.req.query('multimedia_hotlink_template');","sourceCodeStart":79,"sourceCodeEnd":115,"githubUrl":"https://github.com/DIYgod/RSSHub/blob/bed535e0879dc71c5aff6f1e7bd1ac21ede40115/lib/middleware/anti-hotlink.ts#L79-L115","documentation":"Thrown by validateTemplate in the anti-hotlink middleware when a user-supplied hotlink template contains a ${prop} or ${prop_ue} placeholder whose `prop` is not a standard URL object property. The allowed set is exactly: hash, host, hostname, href, origin, password, pathname, port, protocol, search, searchParams, username. The `_ue` suffix requests URL-encoding of the value.","triggerScenarios":"config.feature.allow_user_hotlink_template is true and the request (or config) supplies an image_hotlink_template / multimedia_hotlink_template containing a placeholder like ${foo} or ${host_name} that is not in the allowed set.","commonSituations":"Typo in the template placeholder; assuming a custom field (e.g. ${domain}) exists on the URL object; copying a template from docs that used a property name that was never supported.","solutions":["Replace the invalid placeholder with one of: ${hash} ${host} ${hostname} ${href} ${origin} ${password} ${pathname} ${port} ${protocol} ${search} ${searchParams} ${username}.","Append `_ue` (e.g. ${pathname_ue}) only for URL-encoding, never inside the property name otherwise.","If you do not need a custom template, omit the query param so the config default is used.","Disable allow_user_hotlink_template if templates are not required."],"exampleFix":"// before\n?image_hotlink_template=https://proxy.example.com/?url=${host_name}${pathname}\n// after\n?image_hotlink_template=https://proxy.example.com/?url=${host}${pathname_ue}","handlingStrategy":"validation","validationCode":"const ALLOWED = new Set(['hash','host','hostname','href','origin','password','pathname','port','protocol','search','searchParams','username']);\nconst validateTemplate = (tpl: string) => {\n  for (const m of tpl.matchAll(/\\$\\{([^{}]+)\\}/g)) {\n    const prop = m[1].endsWith('_ue') ? m[1].slice(0, -3) : m[1];\n    if (!ALLOWED.has(prop)) throw new Error(`Invalid URL property: ${prop}`);\n  }\n};\nvalidateTemplate(imageHotlinkTemplate);","typeGuard":"const isPlaceholderValid = (p: string): boolean =>\n  ALLOWED.has(p.endsWith('_ue') ? p.slice(0, -3) : p);","tryCatchPattern":"try { validateTemplate(tpl); } catch (e) {\n  // fall back to the default config template rather than failing the request\n  tpl = undefined;\n}","preventionTips":["Restrict allow_user_hotlink_template to trusted users only.","Provide users a documented allow-list of placeholders.","Prefer ${pathname_ue} over hand-rolled encoding."],"tags":["config","validation","middleware","hotlink"],"backgroundTag":null,"analyzedSha":"bed535e0879dc71c5aff6f1e7bd1ac21ede40115","analyzedAt":"2026-08-12T19:29:35.364Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}