You need to deconstruct the sortstring and check all its parts against a whitelist of allowed terms.
See the following pseudo code. I haven't fully tested it, but lets say it's the idea that counts.
DELIMITER $$
CREATE PROCEDURE `get_users`(IN sortstring TEXT)
BEGIN
//check sortstring against a whitelist of allowed sortstrings
DECLARE sortpart VARCHAR(255);
DECLARE done BOOLEAN DEFAULT false;
DECLARE allok BOOLEAN DEFAULT true;
DECLARE i INTEGER DEFAULT 1;
WHILE ((NOT done) AND allOK) DO
SET sortpart = SUBSTRING_INDEX(sortstring,',',i);
SET i = i + 1;
SET done = (sortpart IS NULL);
IF NOT DONE THEN
SELECT 1 INTO allok WHERE EXISTS
(SELECT 1 FROM whitelist
WHERE allowed_sort_claused = sortpart AND tablename = 'users');
END IF
END WHILE;
IF allOK THEN
PREPARE statement FROM
CONCAT('SELECT username, passhashwithsalt FROM users ',sortstring);
EXECUTE statement;
DEALLOCATE PREPARE statement;
ELSE SELECT 'error' as username, 'error' as passhashwithsalt;
END IF;
END$$
See: How to prevent SQL injection with dynamic tablenames?
The error in your code
You cannot use columnnames or SQL-keywords as parameters. You can only use values as parameters. For that reason your query will never pass prepare.
The ?
in SELECT x FROM t1 ?
wil just be replaced by SELECT x FROM t1 'ORDER BY field1, field2'
Which makes no sense.