{"record":{"id":"a7e6e7a80ad4713d","repo":"lfnovo/open-notebook","slug":"invalid-order-by-clause-clause-strip","errorCode":null,"errorMessage":"Invalid order_by clause: '{clause.strip()}'","messagePattern":"Invalid order_by clause: '(.+?)'","errorType":"validation","errorClass":"InvalidInputError","httpStatus":400,"severity":"warning","filePath":"open_notebook/domain/base.py","lineNumber":63,"sourceCode":"        delegating to `get_all()`) must route through this so the allowlist\n        can't silently drift between call sites.\n        \"\"\"\n        allowed_field_pattern = re.compile(r\"^[a-z_][a-z0-9_]*$\")\n        allowed_directions = {\"asc\", \"desc\"}\n\n        clauses = [c.strip() for c in order_by.split(\",\")]\n        validated_clauses = []\n        for clause in clauses:\n            parts = clause.strip().split()\n            if len(parts) == 1:\n                if not allowed_field_pattern.match(parts[0].lower()):\n                    raise InvalidInputError(f\"Invalid order_by field: '{parts[0]}'\")\n                validated_clauses.append(parts[0].lower())\n            elif len(parts) == 2:\n                if not allowed_field_pattern.match(\n                    parts[0].lower()\n                ) or parts[1].lower() not in allowed_directions:\n                    raise InvalidInputError(\n                        f\"Invalid order_by clause: '{clause.strip()}'\"\n                    )\n                validated_clauses.append(f\"{parts[0].lower()} {parts[1].lower()}\")\n            else:\n                raise InvalidInputError(f\"Invalid order_by clause: '{clause.strip()}'\")\n\n        return \", \".join(validated_clauses)\n\n    @classmethod\n    async def get_all(cls: Type[T], order_by=None) -> List[T]:\n        try:\n            # If called from a specific subclass, use its table_name\n            if cls.table_name:\n                target_class = cls\n                table_name = cls.table_name\n            else:\n                # This path is taken if called directly from ObjectModel\n                raise InvalidInputError(","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/lfnovo/open-notebook/blob/a7de90d38aaf18ee85fd661854d35c11e44613e2/open_notebook/domain/base.py#L45-L81","documentation":"_validate_order_by() rejects multi-token clauses that are malformed: either the field fails the allowed-field pattern or the direction token isn't one of the allowed ASC/DESC keywords; clauses with 3+ tokens always hit this branch. It is the injection/validation guard for the order_by string that gets interpolated into SurrealQL.","triggerScenarios":"Passing 'created_at ASC; DROP' (3+ tokens), 'created-at asc' (bad field), or 'created_at ascending' (direction not in allowed set); concatenating user input into order_by without normalizing; sending empty middle tokens like 'created_at  asc extra'.","commonSituations":"Frontend building sort strings from multiple params; users typing free-text sort expressions; direction values like 'ascending'/'up' instead of 'asc'; injection attempts targeting the ORDER BY clause.","solutions":["Format clauses strictly as '<field> asc|desc' (or bare field), comma-separated for multiple sorts","Map UI sort choices to the two allowed direction keywords before calling get_all","Whitelist the field against the model's fields and reject anything with extra tokens"],"exampleFix":"// before\norder_by = f\"{field} {direction}\"  # direction could be 'ascending'\n// after\nDIRS = {'asc': 'asc', 'ascending': 'asc', 'desc': 'desc', 'descending': 'desc'}\norder_by = f\"{field} {DIRS[direction.lower()]}\"","handlingStrategy":"validation","validationCode":"DIRS = {'asc', 'desc'}\nfor clause in raw.split(','):\n    parts = clause.strip().lower().split()\n    ok = (len(parts) == 1 and parts[0] in allowed_fields) or \\\n         (len(parts) == 2 and parts[0] in allowed_fields and parts[1] in DIRS)\n    if not ok:\n        raise InvalidInputError(f'Invalid order_by clause: {clause}')","typeGuard":"def is_valid_order_clause(clause: str, allowed_fields: set[str]) -> bool:\n    parts = clause.strip().lower().split()\n    return ((len(parts) == 1 and parts[0] in allowed_fields) or\n            (len(parts) == 2 and parts[0] in allowed_fields and parts[1] in ('asc', 'desc')))","tryCatchPattern":"try:\n    items = await Model.get_all(order_by=raw)\nexcept InvalidInputError:\n    items = await Model.get_all()  # fall back to default ordering\n","preventionTips":["Map UI sort selections to exactly '<field> asc|desc' before calling get_all","Normalize common synonyms (ascending→asc) client-side","Reject any clause with more than two tokens at the API boundary"],"tags":["validation","sql-injection","order-by","input-sanitization"],"backgroundTag":"invalid-sort-parameter","analyzedSha":"a7de90d38aaf18ee85fd661854d35c11e44613e2","analyzedAt":"2026-08-27T02:39:58.166Z","schemaVersion":2},"datasetVersion":"2026-08-27T03:17:27.898Z"}