Security Audit Report

Lorem Picsum Security Audit: 5 Findings and 30 Embedded Unsplash URLs

https://picsum.photos/v2/list
89 B Good security posture
5 findings
1 Critical 1 High 1 Medium 2 Low
Share
01

About This API

Lorem Picsum (picsum.photos) is a free, no-auth public service that serves placeholder photographs sized to whatever dimensions the URL requests. It is the open-source successor to lorempixel.com, which went offline in 2021 with effectively no notice and broke roughly half the design-tool tutorials on the internet overnight. Lorem Picsum was running before that happened — the project (built by DMarby on GitHub, source at DMarby/picsum-photos) launched in 2017 — and it absorbed the orphaned ecosystem within weeks of lorempixel's outage.

The service has two distinct surfaces. The image surface — https://picsum.photos/200/300 and similar — is what most consumers use: a request returns a randomly-selected JPEG of the requested size, perfect for filling design mockups, Storybook fixtures, mobile-app prototypes, and 'lorem ipsum but for images' use cases. The metadata surface — https://picsum.photos/v2/list and the per-ID endpoints — returns JSON describing the available images, including each image's photographer, source URL on Unsplash, dimensions, and a Picsum-served download URL.

Picsum's architecture is thin and deliberate: the project curates a list of photos from Unsplash, caches them at the CDN edge, and serves them with deterministic IDs. The /v2/list endpoint is the metadata index. The image endpoints are the bulk of the traffic. This audit covers the metadata endpoint at https://picsum.photos/v2/list, which is the surface most likely to be hit by a programmatic consumer.

02

Threat Model

The threat model for Lorem Picsum is shaped by what the service actually does: it serves cached public photographs from a CDN. There are no credentials to steal, no PII in the responses, no per-user state. The threat surface is essentially zero on the API side.

The propagated-URL pattern

The interesting finding — the MEDIUM 'multiple external URLs in API response' — fires because the /v2/list response embeds 30 Unsplash URLs (one per image item, in the url field). On Picsum's catalog this is fine and intended: the API is a thin metadata layer over Unsplash's public content, and the URL field is part of the documented contract. The propagated risk is a downstream one. A frontend that takes Picsum's response and renders the URLs into <a> tags or <img> srcs without a Content Security Policy inherits Unsplash as a trusted resource origin. If Unsplash ever served compromised content (which has not happened, but is the security-design assumption), the consuming app would render that content without a sandbox.

The HSTS-max-age finding

The LOW 'HSTS max-age too short' finding flags that picsum.photos sends Strict-Transport-Security: max-age=15552000 (180 days). The recommended value for HSTS is one year (31536000 seconds), and the HSTS preload list requires a minimum of one year for inclusion. Bumping the value is a one-line proxy change. The finding is hygiene, not exploitable.

The unauthenticated and rate-limit-headerless findings

The CRITICAL 'no authentication' and HIGH 'no rate-limit headers' are the structural set every public mock produces. Picsum enforces a soft per-IP rate limit at the proxy layer (the documented ceiling is 'reasonable use' without a published number), but consumers cannot see their budget pre-emptively because the response doesn't include X-RateLimit-* headers. As with the other public APIs in this series, this is a hygiene improvement rather than an actionable risk.

03

Methodology

middleBrick ran a black-box scan against https://picsum.photos/v2/list. The scan was read-only — no destructive HTTP methods, no auth headers, no probes against the image surface itself.

Twelve security checks ran. Five produced findings; seven were clean negatives. The five findings:

  • CRITICAL: API accessible without authentication
  • HIGH: missing rate-limit headers
  • MEDIUM: multiple external URLs in API response (30 Unsplash URLs across 1 host)
  • LOW: missing security header (X-Frame-Options)
  • LOW: HSTS max-age too short (15552000s instead of 31536000s)

The clean negatives include the CORS-wildcard finding (Picsum's CORS handling is request-conditional, not a verbatim wildcard), the data-exposure heuristics (no password / token / API-key patterns in response body), the IDOR / sequential-IDs finding (the /v2/list endpoint is a paginated collection, not a numeric-ID-keyed resource), the mass-assignment / property-authorization findings, the X-Powered-By framework disclosure (the Go-based backend doesn't emit it), and the BFLA endpoint-discovery probes (no /admin, /manage, /config, /internal, /health returned 200).

The 'multiple external URLs' finding is one we don't see often. It fires when a response embeds links to a host other than the one being scanned, and Picsum's response includes 30 such links — one per image, all pointing to unsplash.com. The check exists because on a real authenticated API, embedded external URLs are a vector for SSRF-by-proxy, content-injection chains, and cross-origin tracking. On Picsum, the embedded URLs are documented contract, not a finding to fix.

04

Results Overview

Lorem Picsum received a B grade with a score of 89. Five total findings: one CRITICAL, one HIGH, one MEDIUM, two LOW.

The CRITICAL — 'API accessible without authentication' — is the structural finding every unauthenticated public service triggers. It is correct as a structural read but does not represent an actionable risk on this service.

The HIGH — 'missing rate-limit headers' — is structurally true. Picsum enforces a soft limit at the proxy layer but does not emit X-RateLimit-* on the response. The fix is a header at the reverse proxy.

The MEDIUM — 'multiple external URLs in API response (30 across 1 host)' — is the most interesting of the five. The /v2/list response contains 30 image-metadata items, each with a url field pointing to unsplash.com. This is documented and intentional behavior — Picsum is, by design, a metadata + cache layer over Unsplash's public photo content. The finding is correctly raised by the scanner because the pattern (embedded external URLs in an API response) is a real concern on services where the embedded URLs aren't supposed to be there. On Picsum, the embedded URLs are the contract.

The two LOW findings are missing X-Frame-Options (clickjacking-prevention header not emitted) and HSTS max-age below the one-year recommendation. Both are one-line hygiene fixes at the reverse proxy.

05

Detailed Findings

Critical Issues 1
CRITICAL CWE-306

API accessible without authentication

The endpoint returned 200 without any authentication credentials.

Remediation

Implement authentication (API key, OAuth 2.0, or JWT) for all API endpoints.

authentication
High Severity 1
HIGH CWE-770

Missing rate limiting headers

Response contains no X-RateLimit-* or Retry-After headers. Without rate limiting, the API is vulnerable to resource exhaustion attacks (DoS, brute force, abuse).

Remediation

Implement rate limiting (token bucket, sliding window) and return X-RateLimit-Limit, X-RateLimit-Remaining, and Retry-After headers.

resourceConsumption
Medium Severity 1
MEDIUM

Multiple external URLs in API response

Response references 30 external URLs across 1 host(s): unsplash.com

Remediation

Validate and sanitize all external URLs. Implement allowlists for trusted third-party services.

unsafeConsumption
Low Severity 2
LOW CWE-693

Missing security headers (1/4)

Missing: X-Frame-Options — clickjacking.

Remediation

Add the following headers to all API responses: x-frame-options.

authentication
LOW

HSTS max-age is too short

HSTS max-age is 15552000 seconds (recommended: 31536000 / 1 year).

Remediation

Set Strict-Transport-Security max-age to at least 31536000 (1 year).

encryption
06

Attacker Perspective

The attacker reading this audit has very little to do against Picsum directly. The image surface is a cache of public Unsplash content, and the metadata surface is a thin index. Compromise of the service yields nothing that wasn't already public on Unsplash. The interesting attack surface is, again, the consuming applications.

The downstream-CSP attack

The most actionable consideration is on the consumer side. An app that fetches /v2/list and renders the embedded Unsplash URLs into <img> srcs or <a href> attributes inherits Unsplash as a trusted resource origin. The defense is a strict Content Security Policy that whitelists exactly the origins the app intends to render content from. Most tutorials that use Picsum do not set up a CSP; the propagated risk is that those tutorials become production apps that render Unsplash URLs without restriction.

The cache-poisoning consideration

Picsum's CDN-edge caching is its primary scaling mechanism. The maintainer's documented architecture is that images are pulled from Unsplash, cached at the CDN, and served with deterministic IDs. An attacker who could poison the cache for a specific Picsum ID could, in principle, replace the cached image with arbitrary content. There is no evidence this is exploitable — the CDN cache key is, presumably, scoped to the Picsum-controlled URL, and the upstream fetch is to a Picsum-controlled Unsplash account — but the attack model is worth knowing about because it's the only theoretically interesting surface on the service.

The rate-limit-aware abuse

Without published rate-limit headers, an attacker doing high-volume image scraping has no protocol-level signal to back off and will receive 429s without warning. Practically, this means abuse is easier to detect server-side (every aggressive scraper hits the rate-limit wall hard) but harder for legitimate consumers to avoid. The maintainer benefits from this asymmetry; legitimate consumers do not.

07

Analysis

The 'multiple external URLs' finding is the one worth reading carefully. Here's a representative slice of the /v2/list response:

[
  {
    "id": "0",
    "author": "Alejandro Escamilla",
    "width": 5616,
    "height": 3744,
    "url": "https://unsplash.com/photos/yC-Yzbqy7PY",
    "download_url": "https://picsum.photos/id/0/5616/3744"
  },
  ...
]

The url field on each item is the canonical Unsplash photo page for the original image. The download_url is the Picsum-served URL for the cached version sized to the original dimensions. Both are intentional. The scanner's heuristic correctly observes that the response contains links to a host other than the one being scanned and flags the pattern; the severity in context is muted because the pattern is the documented contract.

The HSTS-too-short finding fires because strict-transport-security: max-age=15552000; includeSubDomains is below the 31536000 recommendation. The HSTS preload list (the browser-shipped allowlist that auto-enforces HSTS even before a first connection) requires max-age=31536000 minimum. Picsum is not currently in the preload list (we did not check directly, but the short max-age is consistent with the project not being preloaded). Bumping the value to one year and submitting to hstspreload.org is the standard hardening path.

The X-Frame-Options finding is similarly hygiene. The header prevents clickjacking by disallowing the response from being rendered in an iframe. On a JSON API endpoint (which is rendered by browsers as a download or as raw text rather than as a page), the practical risk is low. The header is still standard hygiene and a one-line fix.

08

Industry Context

Lorem Picsum sits in the placeholder-image API niche essentially alone since lorempixel.com went down in 2021. The closest peers are Unsplash's official public API (which Picsum is a thin layer over), placeholder.com (still operational, smaller catalog), and various proprietary CDN-backed placeholder services. Picsum's positioning — open-source, free, fast, deterministic IDs — has made it the de facto standard for the design-tool tutorial use case.

Compared to other public APIs we've audited in this series, Picsum's findings profile is unusually clean. Five findings is closer to SWAPI's four than to ReqRes's seventeen, and the MEDIUM external-URLs finding is the only one in the entire pool that actually maps to a real architectural choice rather than a generic structural pattern.

For compliance: there is no real PII in the responses (the metadata is photographer attribution, image dimensions, and URLs), so GDPR / CCPA / HIPAA are not in scope for the API surface. Image rights are governed by Unsplash's license terms, which are permissive for most use cases but worth reading before commercial use.

OWASP API Top 10 2023 mapping: API1 (broken object-level authorization) is technically present (the no-auth + sequential-ID pattern) but structurally — the catalog is meant to be world-readable. API8 (security misconfiguration) covers the missing rate-limit headers, missing X-Frame-Options, and short HSTS. API10 (unsafe consumption of APIs) is the closest fit for the embedded-URLs pattern, although the scanner's lens here is on the producer side rather than the consumer side. The remaining categories don't apply to a static-content service.

09

Remediation Guide

Missing rate-limit headers

Emit X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and Retry-After matching the soft per-IP ceiling. Picsum's Go backend already enforces the limit; this just exposes the budget.

// Go HTTP middleware example
func RateLimitHeaders(limit int) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            remaining := getRemainingForIP(r.RemoteAddr)
            w.Header().Set("X-RateLimit-Limit", strconv.Itoa(limit))
            w.Header().Set("X-RateLimit-Remaining", strconv.Itoa(remaining))
            if remaining == 0 {
                w.Header().Set("Retry-After", "60")
            }
            next.ServeHTTP(w, r)
        })
    }
}

HSTS max-age too short

Bump to 31536000 (1 year), include subdomains, mark preload-eligible, submit to hstspreload.org.

// Caddy config example
picsum.photos {
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Frame-Options "DENY"
    }
    reverse_proxy backend:8080
}

Missing X-Frame-Options

Add the header at the reverse proxy. DENY for a JSON API surface.

# nginx example
add_header X-Frame-Options "DENY" always;

Embedded external URLs (consumer-side defense)

Set a strict Content Security Policy on the consuming app that only allows the image hosts you intend to render.

<!-- Strict CSP for an app rendering Picsum's download_url only -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src picsum.photos; script-src 'self'">

<!-- If rendering the original Unsplash URLs from /v2/list, also allow them -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src picsum.photos images.unsplash.com; script-src 'self'">
10

Defense in Depth

For Picsum's maintainer, the action items are short and optional.

1. Bump HSTS to one year. One-line change at the reverse proxy: strict-transport-security: max-age=31536000; includeSubDomains; preload. Then submit to hstspreload.org. Closes one LOW finding and gets the project preloaded.

2. Add X-Frame-Options. X-Frame-Options: DENY at the reverse proxy. Closes the second LOW finding.

3. Emit rate-limit headers. The HIGH finding goes away once X-RateLimit-Limit, X-RateLimit-Remaining, and Retry-After are emitted on responses. Picsum's Go backend already enforces the rate limit; adding the headers is a small wrapper change.

4. The 'external URLs' finding does not need fixing. The pattern is the documented contract; the right defense is on the consumer side, not the producer.

For consumers — apps and tutorials that use Picsum — the defenses are:

(a) Set a strict Content Security Policy that whitelists only the image hosts you intend to render. If you are using Picsum's download_url (the Picsum-hosted version), your CSP only needs to allow picsum.photos; if you render the original Unsplash URLs, also allow images.unsplash.com.

(b) Cache responses aggressively. Picsum's metadata is essentially canonical and changes rarely; the data is highly cacheable on the consumer side.

(c) Don't depend on the service for production-critical paths without a fallback. Picsum is reliable but it is a free fan service; a critical app should have a contingency plan for the case where the service is unavailable.

11

Conclusion

Lorem Picsum scored 89 with five findings because it is a small, well-scoped public service with an unusual architectural choice (the metadata surface embeds Unsplash URLs by design) that the scanner correctly flags as a pattern even though it's documented contract. The two LOW findings are one-line hygiene fixes; the HIGH is a small wrapper change to expose the rate-limit budget to consumers; the MEDIUM is the architecture and doesn't need fixing.

For consumers, the API is exactly what it presents itself as: the placeholder-image service that runs essentially every design-tool tutorial on the internet. Use it. Set a strict CSP on the consuming app. Cache aggressively.

For maintainers building thin metadata APIs over external content sources, Picsum's architecture is a worked example of how to do it: deterministic IDs, edge caching, documented contract. The 'embedded external URLs' finding is the price of being a thin layer; the way to handle it is to document the contract clearly and let consumers build their CSP accordingly.

Frequently Asked Questions

Is Lorem Picsum safe to use in production?
For non-critical paths (design-tool prototypes, mockups, fallback placeholders), yes. For revenue-critical paths, you should have a contingency plan because Picsum is a free service without an SLA — if it goes down, your app should degrade gracefully rather than break.
Why does the audit flag 30 external URLs as a MEDIUM finding?
Because on a typical authenticated API, embedded external URLs are a vector for SSRF-by-proxy and cross-origin tracking. On Picsum, the embedded URLs are intentional — the API is a thin metadata layer over Unsplash. The scanner correctly flagged the pattern; the severity in context is muted.
Should I render the Unsplash URL or the Picsum URL?
If you want the photo at its original dimensions and don't mind the extra DNS hop, the Unsplash URL works. If you want a Picsum-cached version sized to specific dimensions, use the download_url field. Most consumers should use download_url because the caching is faster and the dimensions are deterministic.
Is there an authenticated tier?
No. Picsum is open-source and free. The maintainer accepts donations via the project's GitHub Sponsors but does not offer a paid commercial tier.
What replaced lorempixel.com?
Picsum did. Lorempixel went down in 2021 with no notice and broke a large fraction of the design-tool ecosystem. Picsum was already running and absorbed the orphaned consumers within weeks. If you have legacy lorempixel URLs in old code, the migration path is to replace 'lorempixel.com/{w}/{h}' with 'picsum.photos/{w}/{h}'.