jackwener/OpenCLI · error · CommandExecutionError
${label} returned an untrusted Pixiv image URL
Error message
${label} returned an untrusted Pixiv image URL What it means
This CommandExecutionError is thrown by parsePixivImageUrl in clis/pixiv/bookmark-download.js:44-46 as a security check: the URL parses but is not a trusted Pixiv CDN image. It must satisfy ALL of: https: protocol, hostname i.pximg.net, no embedded username/password, no explicit port, and a file extension mapped in IMAGE_CONTENT_TYPES (.jpg/.jpeg/.png/.gif/.webp). Any violation rejects the URL to prevent SSRF or downloading unexpected content.
Source
Thrown at clis/pixiv/bookmark-download.js:45
if (value !== true) {
throw new ArgumentError('Refusing to write local Pixiv downloads: pass --execute');
}
}
function parsePixivImageUrl(value, label) {
if (typeof value !== 'string' || !value) {
throw new CommandExecutionError(`${label} returned a missing image URL`);
}
let url;
try {
url = new URL(value);
} catch {
throw new CommandExecutionError(`${label} returned a malformed image URL`);
}
const extension = path.extname(url.pathname).toLowerCase();
const contentType = IMAGE_CONTENT_TYPES.get(extension);
if (url.protocol !== 'https:' || url.hostname !== 'i.pximg.net' || url.username || url.password || url.port || !contentType) {
throw new CommandExecutionError(`${label} returned an untrusted Pixiv image URL`);
}
return { url: url.href, extension, contentType };
}
async function prepareIllustPlan(page, row, outputRoot) {
const pages = await pixivFetch(page, `/ajax/illust/${row.illust_id}/pages`, {
notFoundMsg: `Illustration not found: ${row.illust_id}`,
});
if (!Array.isArray(pages)) {
throw new CommandExecutionError('Pixiv pages API returned malformed payload');
}
if (pages.length === 0) {
throw new EmptyResultError('pixiv bookmark-download', `No images found for illustration ${row.illust_id}.`);
}
const files = pages.map((entry, index) => {
if (!entry || Array.isArray(entry) || typeof entry !== 'object' || !entry.urls || Array.isArray(entry.urls) || typeof entry.urls !== 'object') {
throw new CommandExecutionError(`Pixiv illustration ${row.illust_id} returned malformed page ${index + 1}`);
}View on GitHub (pinned to 49907e53dc)
Solutions
- Log the offending URL and check which condition failed (scheme, host, credentials, port, or extension)
- Confirm the content type is actually an image — ugoira/zip or non-image assets are intentionally rejected by this command
- Re-authenticate and retry: restricted sessions can make Pixiv return alternate hosts
- If Pixiv legitimately changed its CDN host or added extensions, update the hostname allowlist or IMAGE_CONTENT_TYPES map accordingly
Defensive patterns
Strategy: validation
Validate before calling
function isTrustedPixivImageUrl(value) {
if (typeof value !== 'string' || !value) return false;
let url;
try { url = new URL(value); } catch { return false; }
const okExt = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].includes(
require('node:path').extname(url.pathname).toLowerCase());
return url.protocol === 'https:' &&
url.hostname === 'i.pximg.net' &&
!url.username && !url.password && !url.port && okExt;
}
if (!isTrustedPixivImageUrl(raw)) console.warn('Untrusted image URL:', raw); Type guard
const TRUSTED_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp']);
function isPximgImageUrl(value) {
let url;
try { url = new URL(value); } catch { return false; }
return url.protocol === 'https:' &&
url.hostname === 'i.pximg.net' &&
!url.username && !url.password && !url.port &&
TRUSTED_EXTENSIONS.has(path.extname(url.pathname).toLowerCase());
} Try / catch
try {
const parsed = parsePixivImageUrl(value, label);
} catch (e) {
if (e instanceof CommandExecutionError && e.message.includes('untrusted Pixiv image URL')) {
console.warn(`${label}: URL failed the pximg allowlist — skipping (possible ugoira/non-image asset)`);
} else {
throw e;
}
} Prevention
- Only download plain image works; route ugoira/zip assets through a dedicated handler
- Never weaken the hostname/protocol allowlist — keep downloads restricted to https + i.pximg.net
- Log the rejected URL to diagnose whether the host, scheme, port, or extension failed
- After Pixiv CDN changes, update the allowlist deliberately rather than disabling the check
- Re-authenticate if restricted sessions cause alternate hosts to appear in payloads
When it happens
Trigger: The pages API returns an image URL that is http:, points at a different host (e.g. embed.pixiv.net or a mirrored host), embeds credentials, specifies a port, or has an unmapped extension (e.g. .zip, no extension), failing the guard at clis/pixiv/bookmark-download.js:44.
Common situations: Pixiv serving ugoira/animation entries whose sources are not plain images; novels or other asset types leaking into the illust download path; a changed CDN hostname after a Pixiv update; URLs from a third-party mirror or proxy injected into the response; extension-less URLs for certain content types.
Related errors
- LinkedIn messengerMessages discovery returned an unsafe or m
- xiaohongshu/delete-note: note URL must be an exact https://*
- xiaohongshu/follow: profile URL must be an exact https://*.x
- Invalid ${label}: ${raw}
- Bilibili summary URL must use http or https
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/4aaeb95f285c4a21.
Report an issue: GitHub.