{"id":"33090e04f38334b4","repo":"psycopg/psycopg2","slug":"invalid-name-name-r","errorCode":null,"errorMessage":"invalid name: {name!r}","messagePattern":"invalid name: (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"lib/sql.py","lineNumber":428,"sourceCode":"\n        >>> q1 = sql.SQL(\"insert into table ({}) values ({})\").format(\n        ...     sql.SQL(', ').join(map(sql.Identifier, names)),\n        ...     sql.SQL(', ').join(sql.Placeholder() * len(names)))\n        >>> print(q1.as_string(conn))\n        insert into table (\"foo\", \"bar\", \"baz\") values (%s, %s, %s)\n\n        >>> q2 = sql.SQL(\"insert into table ({}) values ({})\").format(\n        ...     sql.SQL(', ').join(map(sql.Identifier, names)),\n        ...     sql.SQL(', ').join(map(sql.Placeholder, names)))\n        >>> print(q2.as_string(conn))\n        insert into table (\"foo\", \"bar\", \"baz\") values (%(foo)s, %(bar)s, %(baz)s)\n\n    \"\"\"\n\n    def __init__(self, name=None):\n        if isinstance(name, str):\n            if ')' in name:\n                raise ValueError(f\"invalid name: {name!r}\")\n\n        elif name is not None:\n            raise TypeError(f\"expected string or None as name, got {name!r}\")\n\n        super().__init__(name)\n\n    @property\n    def name(self):\n        \"\"\"The name of the `!Placeholder`.\"\"\"\n        return self._wrapped\n\n    def __repr__(self):\n        if self._wrapped is None:\n            return f\"{self.__class__.__name__}()\"\n        else:\n            return f\"{self.__class__.__name__}({self._wrapped!r})\"\n\n    def as_string(self, context):","sourceCodeStart":410,"sourceCodeEnd":446,"githubUrl":"https://github.com/psycopg/psycopg2/blob/3a6d9d6ddc6b53eaa80b712f5fa6b23abbdc38db/lib/sql.py#L410-L446","documentation":"Raised by Placeholder.__init__() (lib/sql.py:428) when a named placeholder's name string contains ')'. Named placeholders render as %(name)s, so a ')' would prematurely close the placeholder and break the query syntax (and could enable injection). The constructor scans the name and rejects any containing ')' with ValueError before storing it.","triggerScenarios":"sql.Placeholder('foo)bar'), sql.Placeholder('a); drop'), or any sql.Placeholder(name) where name contains a ')' character.","commonSituations":"Building placeholder names from untrusted/unsanitized user input; concatenating user-provided identifiers into placeholder names; copy-paste that includes a closing paren.","solutions":["Sanitize placeholder names: allow only [A-Za-z0-9_] and reject/replace everything else.","Generate placeholder names from a controlled allowlist or sequential counter rather than user input.","Validate the name does not contain ')' (and ideally any non-identifier char) before passing it in."],"exampleFix":"// before\nph = sql.Placeholder(user_input)\n// after\nimport re\nsafe = re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*', user_input)\nif not safe:\n    raise ValueError('bad placeholder name')\nph = sql.Placeholder(user_input)","handlingStrategy":"validation","validationCode":"import re\n\n_SAFE_NAME = re.compile(r'[A-Za-z_][A-Za-z0-9_]*')\n\ndef safe_placeholder(name):\n    if not (isinstance(name, str) and _SAFE_NAME.fullmatch(name)):\n        raise ValueError(f'unsafe placeholder name: {name!r}')\n    return sql.Placeholder(name)","typeGuard":"import re\n\ndef is_valid_placeholder_name(name) -> bool:\n    return (\n        isinstance(name, str)\n        and ')' not in name\n        and re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*', name) is not None\n    )","tryCatchPattern":null,"preventionTips":["Generate placeholder names from a controlled counter or allowlist, never raw user input.","Restrict placeholder names to [A-Za-z0-9_] and require a non-digit first char.","Treat ')' in a name as an injection red flag and reject loudly."],"tags":["psycopg2","sql-composition","placeholder","injection","valueerror"],"analyzedSha":"3a6d9d6ddc6b53eaa80b712f5fa6b23abbdc38db","analyzedAt":"2026-08-04T19:56:51.958Z","schemaVersion":2}