Skip to content

[BUG]: Phishing URL injection via improper hostname validation in LinkedInProfileValidator #87

Description

@Haseebx162006

Description

LinkedInProfileValidator (src/ghdcbot/core/social_validators.py) me hostname validation missing hai. Validator sirf check karta hai ke URL me /in/ maujood hai ya nahi, lekin yeh verify nahi karta ke domain linkedin.com hai.

Impact

  • Severity: High (CWE-20: Improper Input Validation, CWE-601: URL Redirection to Untrusted Site)
  • Attack Scenario: An attacker links a malicious credential-harvesting site (e.g. https://evil-phishing.com/in/target) via /connect-social platform:LinkedIn profile:https://evil-phishing.com/in/target.
  • Public Visibility: The URL is displayed in Discord /profile embeds, where community members may trust and click it as a verified LinkedIn profile.

Technical Details & Root Cause

In src/ghdcbot/core/social_validators.py:

@staticmethod
def _normalize_url(url: str) -> str:
    url = url.strip()
    
    # Ensure https
    if url.startswith("http://"):
        url = "https://" + url[7:]
    elif not url.startswith("https://"):
        url = "https://" + url
    
    # Remove www if present (normalize to linkedin.com)
    url = url.replace("www.linkedin.com", "linkedin.com")
    
    # Remove trailing slashes and query params
    url = url.split("?")[0].rstrip("/")
    
    # Reject company pages
    if "/company/" in url or "/companies/" in url or "/school/" in url:
        raise ValueError("Company and school pages are not supported, only personal profiles")
    
    # Must be in /in/ path for personal profiles
    if "/in/" not in url:
        raise ValueError("Only LinkedIn profile URLs (linkedin.com/in/...) are supported")
    
    return url
Key Flaws:
No Hostname Check: The validator relies purely on string manipulation (.replace("www.linkedin.com", "linkedin.com")) and a substring check (if "/in/" not in url:).
Inconsistency with other validators: In contrast, XProfileValidator uses urllib.parse.urlparse to strictly assert:
python

if host in {"x.com", "twitter.com"}:
Spoofing Vulnerability: URLs such as https://linkedin.com.attacker.com/in/victim or https://evil-phishing.com/in/victim pass _normalize_url and _extract_profile_id.
Exception Handling: Rejection currently relies on downstream Pydantic model initialization, which raises an unhandled pydantic_core.ValidationError rather than a clean, user-friendly ValueError expected by the command handler.

### Steps to Reproduce

from ghdcbot.core.social_validators import LinkedInProfileValidator

validator = LinkedInProfileValidator()
normalized = validator._normalize_url("https://evil-phishing.com/in/target")
profile_id = validator._extract_profile_id("https://evil-phishing.com/in/target")

print("Normalized:", normalized)  # Output: https://evil-phishing.com/in/target
print("Profile ID:", profile_id)  # Output: target

## Proposed Fix

### 1. In `src/ghdcbot/core/social_validators.py` (`LinkedInProfileValidator`):
- Parse the URL using `urllib.parse.urlparse`.
- Validate that the scheme is `http` or `https` (and canonicalize to `https://`).
- Extract `hostname`, strip leading `www.`, and enforce that it strictly matches `linkedin.com`.
- Validate that the path strictly matches `/in/<profile-id>`.
- Raise clean `ValueError` exceptions with descriptive error messages.

### 2. In `tests/test_social_validators.py`:
- Add unit tests verifying rejection of:
  - `https://evil-phishing.com/in/target`
  - `https://linkedin.com.attacker.org/in/target`
  - `https://fake-linkedin.com/in/target`
  - Malicious schemes (e.g., `javascript:`, `ftp:`)

---

## Acceptance Criteria
- [ ] `LinkedInProfileValidator.validate()` rejects non-LinkedIn domains containing `/in/`.
- [ ] Subdomain spoofing (e.g. `linkedin.com.attacker.com`) is rejected with a `ValueError`.
- [ ] Valid LinkedIn formats (`https://linkedin.com/in/user`, `https://www.linkedin.com/in/user`, `linkedin.com/in/user`) continue to pass.
- [ ] All test suites pass cleanly (`pytest tests/test_social_validators.py`).

### Logs and Screenshots

_No response_

### Environment Details

_No response_

### Impact

High - Major feature is broken

### Code of Conduct

- [x] I have joined the [Discord server](https://discord.gg/hjUhu33uAn) and will post updates there
- [x] I have searched existing issues to avoid duplicates

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions