{"record":{"id":"ee74663703b8de53","repo":"lfnovo/open-notebook","slug":"invalid-order-by-field-parts-0","errorCode":null,"errorMessage":"Invalid order_by field: '{parts[0]}'","messagePattern":"Invalid order_by field: '(.+?)'","errorType":"validation","errorClass":"InvalidInputError","httpStatus":400,"severity":"warning","filePath":"open_notebook/domain/base.py","lineNumber":57,"sourceCode":"    @classmethod\n    def _validate_order_by(cls, order_by: str) -> str:\n        \"\"\"Validate and normalize an ORDER BY clause to prevent SurrealQL injection.\n\n        Supports: \"field\", \"field direction\", \"field1 direction, field2 direction\".\n        Any subclass that builds its own query around `order_by` (instead of\n        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","sourceCodeStart":39,"sourceCodeEnd":75,"githubUrl":"https://github.com/lfnovo/open-notebook/blob/a7de90d38aaf18ee85fd661854d35c11e44613e2/open_notebook/domain/base.py#L39-L75","documentation":"_validate_order_by() parses a user-supplied order_by string into field/direction clauses; a single-token clause whose field doesn't match the allowed field pattern raises InvalidInputError('Invalid order_by field: ...'). This is an injection guard — order_by is interpolated into SurrealQL, so only whitelisted field names (and ASC/DESC directions) survive.","triggerScenarios":"Calling get_all(order_by='created; REMOVE TABLE notes') or any single-token clause with punctuation, spaces, or a leading digit; requesting a field not in the model's allowed fields (e.g. ordering notes by 'foo_bar'); passing raw query params straight from the API to get_all.","commonSituations":"Frontend sending a sort field the backend model doesn't define; typos in sort keys ('creatd_at'); attempts at SurrealQL injection through the sort parameter; renaming a model field without updating sort options in the UI.","solutions":["Use only real, whitelisted field names from the model's allowed fields — check the model class for the permitted pattern","Fix typos/case: fields are matched lowercased, so send lowercase snake_case names","If exposing order_by via API, validate it against the model's field list before calling get_all"],"exampleFix":"// before\nNote.get_all(order_by=request.query_params['sort'])\n// after\nallowed = {'created_at', 'updated_at', 'title'}\nsort = request.query_params.get('sort', 'created_at')\nif sort.lstrip('-').split(' ')[0] not in allowed:\n    raise InvalidInputError(f'Invalid order_by field: {sort}')\nNote.get_all(order_by=sort)","handlingStrategy":"validation","validationCode":"allowed = get_allowed_order_fields(Note)  # from the model\nraw = request.query_params.get('order_by', '')\nfor clause in raw.split(','):\n    field = clause.strip().split()[0].lower()\n    if field not in allowed:\n        raise InvalidInputError(f'Invalid order_by field: {field}')","typeGuard":"def is_valid_order_by(order_by: str, allowed_fields: set[str]) -> bool:\n    for clause in order_by.split(','):\n        parts = clause.strip().lower().split()\n        if not parts or parts[0] not in allowed_fields:\n            return False\n        if len(parts) == 2 and parts[1] not in ('asc', 'desc'):\n            return False\n        if len(parts) > 2:\n            return False\n    return True","tryCatchPattern":"try:\n    items = await Model.get_all(order_by=raw)\nexcept InvalidInputError:\n    return JSONResponse(400, 'invalid order_by')","preventionTips":["Never pass raw query params as order_by — whitelist first","Keep frontend sort keys in sync with model field names","Return the allowed-fields list to clients so they can validate locally"],"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"}