Introduction
In the jhs-enhance.user.js file, we discovered a critical hardcoded credential vulnerability at line 10812 where an Imgur API Client-ID (d70305e7c3ac5c6) was embedded directly in the authorization header of a fetch call. This userscript, distributed to all users, made the credential trivially extractable by anyone who installed the script or viewed its source code. The ImageRecognitionPlugin's image upload functionality unknowingly exposed this shared API key to potentially thousands of users, creating a perfect storm for API abuse.
The vulnerable code resided in both the source file (src/plugins/image-recognition.js) and the distributed userscript (dist/jhs-enhance.user.js), meaning every installation contained the plaintext credential. Unlike server-side API keys that can be protected with environment variables and access controls, client-side JavaScript is inherently public—making this a textbook case of why credentials should never be embedded in distributed code.
The Vulnerability Explained
The vulnerability existed in the image upload functionality where the code needed to authenticate with Imgur's API to upload images for recognition purposes. Here's the exact vulnerable code from line 10809-10816:
const response = await fetch("https://api.imgur.com/3/image", {
method: "POST",
headers: {
Authorization: "Client-ID d70305e7c3ac5c6" // ← Hardcoded credential!
},
body: formData
});
The problem is glaringly obvious: the Imgur Client-ID d70305e7c3ac5c6 is hardcoded as a string literal in the Authorization header. Since userscripts are distributed as plain JavaScript files, anyone could:
- Install the userscript and open their browser's developer tools
- Search the source for "Client-ID" or "Authorization"
- Extract the credential in under 10 seconds
- Use the stolen Client-ID to make unlimited Imgur API calls
Real-World Exploitation Scenario
Let's walk through a concrete attack using the actual code:
- Attacker installs jhs-enhance.user.js from the distribution repository
- Opens dist/jhs-enhance.user.js:10812 in any text editor
- Extracts
d70305e7c3ac5c6from the Authorization header - Writes a simple script to upload thousands of images:
// Attacker's exploit script
for (let i = 0; i < 10000; i++) {
fetch("https://api.imgur.com/3/image", {
method: "POST",
headers: {
Authorization: "Client-ID d70305e7c3ac5c6" // Stolen from userscript
},
body: generateSpamImage()
});
}
The impact is severe:
- Quota exhaustion: The legitimate application's Imgur API quota gets consumed by attackers
- Service disruption: Real users can't upload images when the quota is exhausted
- Cost implications: If the API has paid tiers, unauthorized usage could incur charges
- Reputation damage: Imgur might ban the Client-ID for abuse, breaking the feature for all users
The ImageRecognitionPlugin's image-recognition.js file handles user-submitted images for reverse image search functionality. When a user wants to search for an image, the plugin uploads it to Imgur to generate a public URL, then uses that URL with search engines. This workflow requires Imgur API authentication—but sharing one credential across all users created a single point of failure.
The Fix
The fix fundamentally changes the credential management approach by shifting responsibility from the application to individual users. Here's the before-and-after comparison:
Before (Vulnerable):
const response = await fetch("https://api.imgur.com/3/image", {
method: "POST",
headers: {
Authorization: "Client-ID d70305e7c3ac5c6" // Shared, hardcoded credential
},
body: formData
});
After (Secure):
const idKey = "jhs_imgurClientId";
let clientId = localStorage.getItem(idKey);
if (!clientId) {
clientId = window.prompt("请输入您自己的Imgur Client-ID(可前往 https://api.imgur.com/oauth2/addclient 免费申请)用于图片上传搜索:");
if (!clientId) throw new Error("未提供Imgur Client-ID,无法上传图片");
localStorage.setItem(idKey, clientId);
}
const response = await fetch("https://api.imgur.com/3/image", {
method: "POST",
headers: {
Authorization: `Client-ID ${clientId}` // User-provided credential
},
body: formData
});
How This Fix Solves the Problem
The fix implements a user-provided credential model with three key security improvements:
-
No hardcoded secrets: The Client-ID is no longer embedded in the code. Line 10812 now uses a variable
${clientId}instead of the hardcoded string. -
User-specific credentials: Each user must provide their own Imgur Client-ID by visiting
https://api.imgur.com/oauth2/addclientand registering for a free API key. The prompt message (in Chinese) guides users through this process. -
Persistent storage: The Client-ID is stored in
localStorageunder the keyjhs_imgurClientId, so users only need to enter it once. Subsequent uploads reuse the stored credential. -
Fail-safe behavior: If the user cancels the prompt without providing a Client-ID, the code throws an error ("未提供Imgur Client-ID,无法上传图片") rather than attempting an unauthenticated request.
Why Both Files Were Changed
The fix modified two files because the codebase maintains both source and distribution versions:
src/plugins/image-recognition.js: The source file where developers work. This ensures future builds include the fix.dist/jhs-enhance.user.js: The compiled/distributed userscript that users actually install. This provides immediate protection for existing installations.
Both files contained identical vulnerable code at their respective authorization header lines, so both required the same fix to eliminate the hardcoded credential completely.
Prevention & Best Practices
Never Embed Secrets in Client-Side Code
The fundamental principle: anything in client-side JavaScript is public. This includes:
- API keys and Client-IDs
- OAuth tokens
- Encryption keys
- Database credentials
- Service URLs with embedded authentication
Even obfuscation or minification provides zero security—deobfuscation tools can reverse any JavaScript transformation in seconds.
Secure Alternatives for API Authentication
When building userscripts or browser extensions that need API access, use these patterns:
-
User-provided credentials (as implemented in this fix):
- Each user registers for their own API key
- Store credentials inlocalStorageorchrome.storage
- Pros: Simple, no backend needed
- Cons: Requires user effort, exposes individual keys -
Backend proxy:
javascript // Instead of calling Imgur directly: const response = await fetch("https://your-backend.com/api/upload-image", { method: "POST", body: formData }); // Your backend handles Imgur authentication server-side
- Pros: Credentials never leave your server
- Cons: Requires infrastructure, adds latency -
OAuth flows:
- Use OAuth 2.0 to let users authorize your app
- Store refresh tokens securely (server-side or encrypted client-side)
- Pros: Industry standard, user revocable
- Cons: Complex implementation
Detection and Prevention Tools
Protect your codebase from credential leaks with these tools:
- Secret scanners: Tools like TruffleHog, git-secrets, or detect-secrets scan commits for credential patterns
- Pre-commit hooks: Block commits containing strings like "Client-ID", "Authorization: Bearer", or API key patterns
- Static analysis: Use Semgrep rules to detect hardcoded credentials in authorization headers
- Code review: Train developers to recognize credential patterns and flag them in reviews
OWASP Recommendations
This vulnerability maps to several OWASP guidelines:
- OWASP Top 10 2021 A07:2021 – Identification and Authentication Failures: Hardcoded credentials represent a critical authentication failure
- OWASP Application Security Verification Standard (ASVS) V2.10: "Verify that application secrets are not stored in client-side code"
- OWASP Secrets Management Cheat Sheet: Recommends environment variables, vaults, or user-provided secrets over hardcoded values
Key Takeaways
- The jhs-enhance.user.js userscript exposed the Imgur Client-ID
d70305e7c3ac5c6at line 10812, making it extractable by any user who installed or viewed the script source - Client-side JavaScript is inherently public—any credential embedded in a userscript, browser extension, or web application frontend should be considered compromised
- The fix replaced the hardcoded Authorization header with a localStorage-backed user prompt, requiring each user to provide their own Imgur Client-ID from
https://api.imgur.com/oauth2/addclient - Both src/plugins/image-recognition.js and dist/jhs-enhance.user.js required fixes because the codebase maintains source and distribution versions that both contained the vulnerable code
- User-provided credentials shift API quota responsibility to individual users, preventing a single compromised key from disrupting service for all users
How Orbis AppSec Detected This
- Source: The hardcoded string literal
"Client-ID d70305e7c3ac5c6"embedded in the source code - Sink: The
Authorizationheader in thefetch()call atdist/jhs-enhance.user.js:10812andsrc/plugins/image-recognition.js:165 - Missing control: No mechanism to separate credentials from code; the Client-ID was directly embedded rather than being user-provided or server-managed
- CWE: CWE-798 (Use of Hard-coded Credentials)
- Fix: Replaced the hardcoded Client-ID with a prompt-based system that requests user-provided credentials and stores them in localStorage
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
The hardcoded Imgur Client-ID in jhs-enhance.user.js demonstrates why client-side code can never safely contain secrets. By moving to a user-provided credential model, the fix eliminates the shared secret vulnerability while maintaining functionality. The key lesson: treat all client-side code as public, design authentication flows accordingly, and use tools like Orbis AppSec to catch credential leaks before they reach production. Whether you're building userscripts, browser extensions, or web applications, never embed API keys in code that users can access—your security posture depends on it.