# URL Parsing Security

> Secure URL validation patterns for FDC Web2Json attestations to prevent MitM attacks.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.

Source: https://dev.flare.network/fdc/guides/url-parsing-security

When using the `Web2Json` attestation type, your smart contract receives data along with a proof that includes the source URL. Without proper URL validation, an attacker could submit a valid FDC proof from a malicious source and have your contract accept it as legitimate data.

This guide covers security patterns for parsing and validating URLs in your FDC-enabled smart contracts.

## The Problem: Man-in-the-Middle Attacks[​](#the-problem-man-in-the-middle-attacks "Direct link to The Problem: Man-in-the-Middle Attacks")

FDC proves that data came from a specific URL. It does **not** prove that the URL is the one you intended to use.

Consider a price feed contract that expects data from CoinGecko. An attacker could:

1.  Create a malicious API endpoint that returns manipulated prices
2.  Get a valid FDC attestation for their malicious endpoint
3.  Submit this proof to your contract
4.  Your contract accepts the manipulated data because the FDC proof is cryptographically valid

URL validation ensures your contract only accepts data from trusted sources.

## Security Layers[​](#security-layers "Direct link to Security Layers")

A robust URL validation strategy includes multiple layers:

Layer

Purpose

Example

Protocol

Prevent MitM on the wire

HTTPS only

Host

Restrict to trusted domains

`api.coingecko.com`

Path

Validate the specific endpoint

`/api/v3/coins/bitcoin/history`

Parameters

Verify query parameters match expected values

Asset ID extraction

## Implementation Patterns[​](#implementation-patterns "Direct link to Implementation Patterns")

### Pattern 1: HTTPS Enforcement[​](#pattern-1-https-enforcement "Direct link to Pattern 1: HTTPS Enforcement")

Always require HTTPS to prevent network-level interception.

contracts/UrlSecurity.sol

```
function _validateProtocol(string memory _url) internal pure {    bytes memory urlBytes = bytes(_url);    bytes memory httpsPrefix = bytes("https://");    if (!_startsWith(urlBytes, httpsPrefix)) {        revert InvalidUrlProtocol();    }}function _startsWith(bytes memory data, bytes memory prefix) internal pure returns (bool) {    uint256 prefixLen = prefix.length;    if (data.length < prefixLen) return false;    for (uint256 i = 0; i < prefixLen; ) {        if (data[i] != prefix[i]) return false;        unchecked { ++i; }    }    return true;}
```

### Pattern 2: Host Extraction and Validation[​](#pattern-2-host-extraction-and-validation "Direct link to Pattern 2: Host Extraction and Validation")

Extract the host from the URL and validate against a whitelist.

contracts/UrlSecurity.sol

```
function _extractHost(string memory _url) internal pure returns (string memory) {    bytes memory urlBytes = bytes(_url);    bytes memory httpsPrefix = bytes("https://");    uint256 startIndex = httpsPrefix.length;    uint256 urlLen = urlBytes.length;    uint256 endIndex = urlLen;    // Find the first "/" after the host    for (uint256 i = startIndex; i < urlLen; ) {        if (urlBytes[i] == "/") {            endIndex = i;            break;        }        unchecked { ++i; }    }    return string(_slice(urlBytes, startIndex, endIndex));}function _validateHost(string memory _url) internal pure {    string memory host = _extractHost(_url);    string memory lowerHost = _toLowerCase(host);    bool validHost = _stringsEqual(lowerHost, "api.coingecko.com") ||                     _stringsEqual(lowerHost, "api.trusted-source.com");    if (!validHost) {        revert InvalidUrlHost(_url, host);    }}
```

Case-insensitive comparison prevents bypasses using mixed-case hostnames.

contracts/UrlSecurity.sol

```
function _toLowerCase(string memory str) internal pure returns (string memory) {    bytes memory strBytes = bytes(str);    uint256 len = strBytes.length;    bytes memory result = new bytes(len);    for (uint256 i = 0; i < len; ) {        bytes1 char = strBytes[i];        // Convert A-Z (65-90) to a-z (97-122)        if (char >= 0x41 && char <= 0x5A) {            result[i] = bytes1(uint8(char) + 32);        } else {            result[i] = char;        }        unchecked { ++i; }    }    return string(result);}
```

### Pattern 3: Path Validation with StartsWith[​](#pattern-3-path-validation-with-startswith "Direct link to Pattern 3: Path Validation with StartsWith")

Validate that the path begins with the expected endpoint. Using `startsWith` prevents prefix injection attacks where an attacker creates a path like `/malicious/api/v1/data` that contains your expected path as a substring.

contracts/UrlSecurity.sol

```
function _extractPath(string memory _url) internal pure returns (string memory) {    bytes memory urlBytes = bytes(_url);    bytes memory httpsPrefix = bytes("https://");    uint256 startIndex = httpsPrefix.length;    uint256 urlLen = urlBytes.length;    // Find the first "/" after the host (start of path)    for (uint256 i = startIndex; i < urlLen; ) {        if (urlBytes[i] == "/") {            return string(_slice(urlBytes, i, urlLen));        }        unchecked { ++i; }    }    return "";}function _validatePath(string memory _url, string memory expectedPathPrefix) internal pure {    string memory path = _extractPath(_url);    if (!_startsWith(bytes(path), bytes(expectedPathPrefix))) {        revert InvalidUrlPath(_url, path);    }}
```

### Pattern 4: Parameter Extraction[​](#pattern-4-parameter-extraction "Direct link to Pattern 4: Parameter Extraction")

For APIs like CoinGecko where the asset identifier is embedded in the URL path, extract and validate it.

contracts/UrlSecurity.sol

```
/** * @notice Extracts the asset ID from a URL like "/coins/{id}/history..." * @param _url The full URL string * @return The extracted asset ID */function _extractAssetIdFromUrl(string memory _url) internal pure returns (string memory) {    bytes memory urlBytes = bytes(_url);    bytes memory prefix = bytes("/coins/");    bytes memory suffix = bytes("/history");    uint256 startIndex = _indexOf(urlBytes, prefix);    if (startIndex == type(uint256).max) {        return ""; // Prefix not found    }    startIndex += prefix.length;    uint256 endIndex = _indexOfFrom(urlBytes, suffix, startIndex);    if (endIndex == type(uint256).max) {        endIndex = urlBytes.length;    }    return string(_slice(urlBytes, startIndex, endIndex));}function _indexOf(bytes memory data, bytes memory marker) internal pure returns (uint256) {    uint256 dataLen = data.length;    uint256 markerLen = marker.length;    if (markerLen == 0 || dataLen < markerLen) return type(uint256).max;    for (uint256 i = 0; i <= dataLen - markerLen; i++) {        bool found = true;        for (uint256 j = 0; j < markerLen; j++) {            if (data[i + j] != marker[j]) {                found = false;                break;            }        }        if (found) return i;    }    return type(uint256).max;}function _indexOfFrom(bytes memory data, bytes memory marker, uint256 from) internal pure returns (uint256) {    uint256 dataLen = data.length;    uint256 markerLen = marker.length;    if (markerLen == 0 || dataLen < markerLen) return type(uint256).max;    for (uint256 i = from; i <= dataLen - markerLen; i++) {        bool found = true;        for (uint256 j = 0; j < markerLen; j++) {            if (data[i + j] != marker[j]) {                found = false;                break;            }        }        if (found) return i;    }    return type(uint256).max;}
```

Then validate the extracted ID matches your expected asset:

contracts/UrlSecurity.sol

```
function verifyPrice(IWeb2Json.Proof calldata _proof) external {    // Extract and validate the asset ID from the URL    string memory extractedId = _extractAssetIdFromUrl(_proof.data.requestBody.url);    string memory expectedId = assetIdMapping[expectedSymbol];    if (keccak256(bytes(extractedId)) != keccak256(bytes(expectedId))) {        revert InvalidAssetIdInUrl(_proof.data.requestBody.url, extractedId, expectedId);    }    // Continue with FDC verification...    require(        ContractRegistry.getFdcVerification().verifyWeb2Json(_proof),        "Invalid proof"    );}
```

## Complete Example[​](#complete-example "Direct link to Complete Example")

Here is a complete URL validation function combining all security layers:

contracts/SecureFdcConsumer.sol

```
// SPDX-License-Identifier: MITpragma solidity ^0.8.25;import { ContractRegistry } from "@flarenetwork/flare-periphery-contracts/coston2/ContractRegistry.sol";import { IWeb2Json } from "@flarenetwork/flare-periphery-contracts/coston2/IWeb2Json.sol";contract SecureFdcConsumer {    // Allowed hosts for data sources    string[] public allowedHosts;    // Expected API path prefix    string public expectedPathPrefix;    error InvalidUrlProtocol();    error InvalidUrlHost(string url, string host);    error InvalidUrlPath(string url, string path);    error SliceOutOfBounds();    constructor(string[] memory _allowedHosts, string memory _expectedPath) {        allowedHosts = _allowedHosts;        expectedPathPrefix = _expectedPath;    }    function _validateUrl(string memory _url) internal view {        bytes memory urlBytes = bytes(_url);        // 1. Enforce HTTPS        if (!_startsWith(urlBytes, bytes("https://"))) {            revert InvalidUrlProtocol();        }        // 2. Validate host        string memory host = _extractHost(_url);        string memory lowerHost = _toLowerCase(host);        bool validHost = false;        for (uint256 i = 0; i < allowedHosts.length; i++) {            if (_stringsEqual(lowerHost, allowedHosts[i])) {                validHost = true;                break;            }        }        if (!validHost) {            revert InvalidUrlHost(_url, host);        }        // 3. Validate path        string memory path = _extractPath(_url);        if (!_startsWith(bytes(path), bytes(expectedPathPrefix))) {            revert InvalidUrlPath(_url, path);        }    }    function consumeData(IWeb2Json.Proof calldata _proof) external {        // Validate URL before accepting proof        _validateUrl(_proof.data.requestBody.url);        // Verify FDC proof        require(            ContractRegistry.getFdcVerification().verifyWeb2Json(_proof),            "Invalid proof"        );        // Process the verified data...    }    // Helper functions (see implementations above)    function _startsWith(bytes memory data, bytes memory prefix) internal pure returns (bool) { /* ... */ }    function _extractHost(string memory _url) internal pure returns (string memory) { /* ... */ }    function _extractPath(string memory _url) internal pure returns (string memory) { /* ... */ }    function _toLowerCase(string memory str) internal pure returns (string memory) { /* ... */ }    function _stringsEqual(string memory a, string memory b) internal pure returns (bool) {        return keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b));    }    function _slice(bytes memory data, uint256 start, uint256 end) internal pure returns (bytes memory) { /* ... */ }}
```

## Common Vulnerabilities[​](#common-vulnerabilities "Direct link to Common Vulnerabilities")

### Missing HTTPS Check[​](#missing-https-check "Direct link to Missing HTTPS Check")

Without HTTPS enforcement, an attacker could intercept HTTP requests and modify responses.

```
// ❌ VULNERABLE: Accepts HTTP URLsfunction validateUrl(string memory url) internal pure {    // Only checks host, not protocol    require(_isAllowedHost(url), "Invalid host");}// ✅ SECURE: Requires HTTPSfunction validateUrl(string memory url) internal pure {    require(_startsWith(bytes(url), bytes("https://")), "HTTPS required");    require(_isAllowedHost(url), "Invalid host");}
```

### Case-Sensitive Host Comparison[​](#case-sensitive-host-comparison "Direct link to Case-Sensitive Host Comparison")

DNS is case-insensitive, so `API.COINGECKO.COM` resolves to the same server as `api.coingecko.com`.

```
// ❌ VULNERABLE: Case-sensitive comparisonfunction isValidHost(string memory host) internal pure returns (bool) {    return keccak256(bytes(host)) == keccak256(bytes("api.coingecko.com"));}// ✅ SECURE: Case-insensitive comparisonfunction isValidHost(string memory host) internal pure returns (bool) {    return keccak256(bytes(_toLowerCase(host))) == keccak256(bytes("api.coingecko.com"));}
```

### Contains Instead of StartsWith[​](#contains-instead-of-startswith "Direct link to Contains Instead of StartsWith")

Using `contains` for path validation allows prefix injection attacks.

```
// ❌ VULNERABLE: Contains check allows prefix injection// Attacker could use: /malicious/api/v1/price -> contains "/api/v1/price" = truefunction isValidPath(string memory path) internal pure returns (bool) {    return _contains(bytes(path), bytes("/api/v1/price"));}// ✅ SECURE: StartsWith prevents prefix injectionfunction isValidPath(string memory path) internal pure returns (bool) {    return _startsWith(bytes(path), bytes("/api/v1/price"));}
```

## Gas Considerations[​](#gas-considerations "Direct link to Gas Considerations")

URL parsing in Solidity is gas-intensive due to string operations. Consider these optimizations:

1.  **Cache host comparisons**: Pre-compute keccak256 hashes of allowed hosts
2.  **Limit URL length**: Reject URLs exceeding a maximum length before parsing
3.  **Use assembly**: For production contracts, consider assembly-optimized string operations

contracts/GasOptimized.sol

```
// Pre-computed host hashesbytes32 constant COINGECKO_HOST_HASH = keccak256(bytes("api.coingecko.com"));bytes32 constant BACKUP_HOST_HASH = keccak256(bytes("backup.coingecko.com"));function _isValidHost(string memory host) internal pure returns (bool) {    bytes32 hostHash = keccak256(bytes(_toLowerCase(host)));    return hostHash == COINGECKO_HOST_HASH || hostHash == BACKUP_HOST_HASH;}
```

## Reference Implementations[​](#reference-implementations "Direct link to Reference Implementations")

For production-ready examples, see:

-   [PriceVerifierCustomFeed](https://github.com/flare-foundation/flare-hardhat-starter/blob/main/contracts/customFeeds/PriceVerifierCustomFeed.sol) - Asset ID extraction from CoinGecko URLs

PriceVerifierCustomFeed.sol

```
// SPDX-License-Identifier: MITpragma solidity ^0.8.25;import {ContractRegistry} from "@flarenetwork/flare-periphery-contracts/coston2/ContractRegistry.sol";import {IWeb2Json} from "@flarenetwork/flare-periphery-contracts/coston2/IWeb2Json.sol";import {IICustomFeed} from "@flarenetwork/flare-periphery-contracts/coston2/customFeeds/interfaces/IICustomFeed.sol";struct PriceData {    uint256 price;}/** * @title PriceVerifierCustomFeed * @notice An FTSO Custom Feed contract that sources its value from FDC-verified data using Web2Json. * @dev Implements the IICustomFeed interface and includes verification logic specific to Web2Json API structure. */contract PriceVerifierCustomFeed is IICustomFeed {    // --- State Variables ---    bytes21 public immutable feedIdentifier;    string public expectedSymbol;    int8 public decimals_;    uint256 public latestVerifiedPrice;    address public owner;    mapping(bytes32 => string) public symbolToCoinGeckoId;    // --- Events ---    event PriceVerified(string indexed symbol, uint256 price, string apiUrl);    event UrlParsingCheck(string apiUrl, string coinGeckoId, string dateString);    event CoinGeckoIdMappingSet(bytes32 indexed symbolHash, string coinGeckoId);    // --- Errors ---    error InvalidFeedId();    error InvalidSymbol();    error UrlCoinGeckoIdMismatchExpected();    error CoinGeckoIdParsingFailed();    error UnknownSymbolForCoinGeckoId(); // Kept for direct call if needed, but mapping is primary    error CoinGeckoIdNotMapped(string symbol);    error DateStringParsingFailed();    error InvalidCoinGeckoIdInUrl(        string url,        string extractedId,        string expectedId    );    error InvalidProof();    // --- Constructor ---    constructor(        bytes21 _feedId,        string memory _expectedSymbol,        int8 _decimals    ) {        if (_feedId == bytes21(0)) revert InvalidFeedId();        if (bytes(_expectedSymbol).length == 0) revert InvalidSymbol();        if (_decimals < 0) revert InvalidSymbol();        owner = msg.sender;        feedIdentifier = _feedId;        expectedSymbol = _expectedSymbol;        decimals_ = _decimals;        // Initialize default CoinGecko IDs        _setCoinGeckoIdInternal("BTC", "bitcoin");        _setCoinGeckoIdInternal("ETH", "ethereum");        // Ensure the expected symbol has a mapping at deployment time        require(            bytes(                symbolToCoinGeckoId[                    keccak256(abi.encodePacked(_expectedSymbol))                ]            ).length > 0,            "Initial symbol not mapped"        );    }    // --- Owner Functions ---    /**     * @notice Allows the owner to add or update a CoinGecko ID mapping for a symbol.     * @param _symbol The trading symbol (e.g., "LTC").     * @param _coinGeckoId The corresponding CoinGecko ID (e.g., "litecoin").     */    function setCoinGeckoIdMapping(        string calldata _symbol,        string calldata _coinGeckoId    ) external {        _setCoinGeckoIdInternal(_symbol, _coinGeckoId);    }    function _setCoinGeckoIdInternal(        string memory _symbol,        string memory _coinGeckoId    ) internal {        require(bytes(_symbol).length > 0, "Symbol cannot be empty");        require(bytes(_coinGeckoId).length > 0, "CoinGecko ID cannot be empty");        bytes32 symbolHash = keccak256(abi.encodePacked(_symbol));        symbolToCoinGeckoId[symbolHash] = _coinGeckoId;        emit CoinGeckoIdMappingSet(symbolHash, _coinGeckoId);    }    // --- FDC Verification & Price Logic ---    /**     * @notice Verifies the price data proof and stores the price.     * @dev Uses Web2Json FDC verification. Checks if the symbol in the URL matches expectedSymbol.     * @param _proof The IWeb2Json.Proof data structure.     */    function verifyPrice(IWeb2Json.Proof calldata _proof) external {        // 1. CoinGecko ID Verification (from URL)        string memory extractedCoinGeckoId = _extractCoinGeckoIdFromUrl(            _proof.data.requestBody.url        );        string memory expectedCoinGeckoId = symbolToCoinGeckoId[            keccak256(abi.encodePacked(expectedSymbol))        ];        if (bytes(expectedCoinGeckoId).length == 0) {            revert CoinGeckoIdNotMapped(expectedSymbol);        }        if (            keccak256(abi.encodePacked(extractedCoinGeckoId)) !=            keccak256(abi.encodePacked(expectedCoinGeckoId))        ) {            revert InvalidCoinGeckoIdInUrl(                _proof.data.requestBody.url,                extractedCoinGeckoId,                expectedCoinGeckoId            );        }        // 2. FDC Verification (Web2Json)        // Aligned with the Web2Json.sol example's pattern        require(            ContractRegistry.getFdcVerification().verifyWeb2Json(_proof),            "FDC: Invalid Web2Json proof"        );        // 3. Decode Price Data        // Path changed to _proof.data.responseBody.abiEncodedData        PriceData memory newPriceData = abi.decode(            _proof.data.responseBody.abiEncodedData,            (PriceData)        );        // 4. Store verified data        latestVerifiedPrice = newPriceData.price;        // 5. Emit main event        emit PriceVerified(            expectedSymbol,            newPriceData.price,            _proof.data.requestBody.url // URL from the Web2Json proof        );    }    // --- Custom Feed Logic ---    function read() public view returns (uint256 value) {        value = latestVerifiedPrice;    }    function feedId() external view override returns (bytes21 _feedId) {        _feedId = feedIdentifier;    }    function calculateFee() external pure override returns (uint256 _fee) {        return 0;    }    function getFeedDataView()        external        view        returns (uint256 _value, int8 _decimals)    {        _value = latestVerifiedPrice;        _decimals = decimals_;    }    function getCurrentFeed()        external        payable        override        returns (uint256 _value, int8 _decimals, uint64 _timestamp)    {        _value = latestVerifiedPrice;        _decimals = decimals_;        _timestamp = 0;    }    function decimals() external view returns (int8) {        return decimals_;    }    // --- Internal Helper Functions To Parse URL---    /**     * @notice Helper function to extract a slice of bytes.     * @param data The original bytes array.     * @param start The starting index (inclusive).     * @param end The ending index (exclusive).     * @return The sliced bytes.     */    function slice(        bytes memory data,        uint256 start,        uint256 end    ) internal pure returns (bytes memory) {        require(end >= start, "Slice: end before start");        require(data.length >= end, "Slice: end out of bounds");        bytes memory result = new bytes(end - start);        for (uint256 i = start; i < end; i++) {            result[i - start] = data[i];        }        return result;    }    /**     * @notice Extracts the CoinGecko ID from the API URL.     * @dev It assumes the URL format is like ".../coins/{id}/history..." or ".../coins/{id}"     * @param _url The full URL string from the proof.     * @return The extracted CoinGecko ID.     */    function _extractCoinGeckoIdFromUrl(        string memory _url    ) internal pure returns (string memory) {        bytes memory urlBytes = bytes(_url);        bytes memory prefix = bytes("/coins/");        bytes memory suffix = bytes("/history");        uint256 startIndex = _indexOf(urlBytes, prefix);        if (startIndex == type(uint256).max) {            return ""; // Prefix not found        }        startIndex += prefix.length;        uint256 endIndex = _indexOfFrom(urlBytes, suffix, startIndex);        if (endIndex == type(uint256).max) {            // Suffix not found, assume it's the end of the string            endIndex = urlBytes.length;        }        return string(slice(urlBytes, startIndex, endIndex));    }    /**     * @notice Helper to find the first occurrence of a marker in bytes.     * @param data The bytes data to search in.     * @param marker The bytes marker to find.     * @return The starting index of the marker, or type(uint256).max if not found.     */    function _indexOf(        bytes memory data,        bytes memory marker    ) internal pure returns (uint256) {        uint256 dataLen = data.length;        uint256 markerLen = marker.length;        if (markerLen == 0 || dataLen < markerLen) return type(uint256).max;        for (uint256 i = 0; i <= dataLen - markerLen; i++) {            bool found = true;            for (uint256 j = 0; j < markerLen; j++) {                if (data[i + j] != marker[j]) {                    found = false;                    break;                }            }            if (found) return i;        }        return type(uint256).max;    }    /**     * @notice Helper to find the first occurrence of a marker in bytes, starting from an index.     * @param data The bytes data to search in.     * @param marker The bytes marker to find.     * @param from The index to start searching from.     * @return The starting index of the marker, or type(uint256).max if not found.     */    function _indexOfFrom(        bytes memory data,        bytes memory marker,        uint256 from    ) internal pure returns (uint256) {        uint256 dataLen = data.length;        uint256 markerLen = marker.length;        if (markerLen == 0 || dataLen < markerLen) return type(uint256).max;        for (uint256 i = from; i <= dataLen - markerLen; i++) {            bool found = true;            for (uint256 j = 0; j < markerLen; j++) {                if (data[i + j] != marker[j]) {                    found = false;                    break;                }            }            if (found) return i;        }        return type(uint256).max;    }}
```

## Summary[​](#summary "Direct link to Summary")

Secure URL parsing for FDC requires:

1.  **HTTPS enforcement** - Reject HTTP URLs
2.  **Host whitelisting** - Only accept data from trusted domains
3.  **Case-insensitive comparison** - Prevent bypass via mixed-case hosts
4.  **Path validation with startsWith** - Prevent prefix injection attacks
5.  **Parameter extraction** - Validate embedded identifiers match expected values

Always validate URLs before calling `verifyWeb2Json()`. A valid FDC proof only guarantees the data came from the URL in the proof—it does not guarantee the URL is trustworthy.
