surrealdb / surrealdb.py

SurrealDB SDK for Python
https://surrealdb.com
Apache License 2.0
176 stars 52 forks source link

Query Params support for query methods #37

Closed i1Fury closed 1 year ago

i1Fury commented 1 year ago

Add query params with safety measures against sqli into full query methods(such as the execute method).

Look at how PyMySQL got this done, it will need to be modified a bit because sdb may need some type declarations.

dentropy commented 1 year ago

Out of curiosity do you know of any good sources you personally use for dealing with sqli

i1Fury commented 1 year ago

I wrote a quick fix in my personal mini surrealdb library, that just uses json.dumps()

    def escape(self, obj: Any) -> str:
        """Escape an object to be used in a query.

        Parameters
        ----------
        obj: :class:`Any`
            The object to escape.

        Returns
        -------
        :class:`str`
            The escaped object.
        """
        return dumps(obj, default=str)

    async def format_query(self, query: str, *positional_params, **absolute_params) -> str:
        """Format a query without executing it.

        Parameters
        ----------
        query: :class:`str`
            The query to execute.
        *positional_params: :class:`Any`
            The positional parameters to use for the query.
            Example: ``SELECT * FROM table WHERE id = {}``
        **absolute_params: :class:`Any`
            The absolute parameters to use for the query.
            Example: ``SELECT * FROM table WHERE id = {id}``

        Returns
        -------
        :class:`str`
            The formatted query.
        """
        if query.count("{}") != len(positional_params):
            raise ParamsError(f"Expected {query.count('{}')} positional parameters, got {len(positional_params)}.\nQuery: {query}\nPositional Parameters: {positional_params}")

        for param in absolute_params:
            if f"{{{param}}}" not in query:
                raise ParamsError(f"Expected an absolute parameter named '{param}', got none.\nQuery: {query}\nAbsolute Parameters: {absolute_params}")

        try:
            query = query.format(
                *map(self.escape, positional_params),
                **{k: self.escape(v) for k, v in absolute_params.items()}   
            )
        except (KeyError, IndexError):
            raise ParamsError(f"Invalid parameters.\nQuery: {query}\nPositional: {positional_params}\nAbsolute: {absolute_params}")
        else:
            return query

    async def execute(self, query: str, *positional_params, **absolute_params) -> List[Dict[str, Any]]:
        """Execute a query against the SurrealDB server.

        Parameters
        ----------
        query: :class:`str`
            The query to execute.
        *positional_params: :class:`Any`
            The positional parameters to use for the query.
            Example: ``SELECT * FROM table WHERE id = {}``
        **absolute_params: :class:`Any`
            The absolute parameters to use for the query.
            Example: ``SELECT * FROM table WHERE id = {id}``

        Returns
        -------
        :class:`List[Dict[str, Any]]`
            The results of the query.
        """
        query = await self.format_query(query, *positional_params, **absolute_params)

        try:
            return await super().execute(query)
        except SurrealException as e:
            raise SurrealException(f'\nQuery: {query}\n{"".join(e.args)}') from e
        except ConnectTimeout:
            raise DBConnectionError(f"Failed to connect to the database.\nQuery: {query}")
AlexFrid commented 1 year ago

Thanks for the suggestions ❤️ Query parameters have been implemented in the above PR. Its now functional in the default websocket client, also using json.dumps(). It has also been implemented in the HTTP client as well, but returns an empty array currently as the HTTP query parameter functionality will become active with the beta9 release soon.

Since it has been implemented, I will therefore close this issue.