lfnovo/open-notebook · warning · InvalidInputError

Invalid order_by field: '{parts[0]}'

Error message

Invalid order_by field: '{parts[0]}'

What it means

_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.

Source

Thrown at open_notebook/domain/base.py:57

    @classmethod
    def _validate_order_by(cls, order_by: str) -> str:
        """Validate and normalize an ORDER BY clause to prevent SurrealQL injection.

        Supports: "field", "field direction", "field1 direction, field2 direction".
        Any subclass that builds its own query around `order_by` (instead of
        delegating to `get_all()`) must route through this so the allowlist
        can't silently drift between call sites.
        """
        allowed_field_pattern = re.compile(r"^[a-z_][a-z0-9_]*$")
        allowed_directions = {"asc", "desc"}

        clauses = [c.strip() for c in order_by.split(",")]
        validated_clauses = []
        for clause in clauses:
            parts = clause.strip().split()
            if len(parts) == 1:
                if not allowed_field_pattern.match(parts[0].lower()):
                    raise InvalidInputError(f"Invalid order_by field: '{parts[0]}'")
                validated_clauses.append(parts[0].lower())
            elif len(parts) == 2:
                if not allowed_field_pattern.match(
                    parts[0].lower()
                ) or parts[1].lower() not in allowed_directions:
                    raise InvalidInputError(
                        f"Invalid order_by clause: '{clause.strip()}'"
                    )
                validated_clauses.append(f"{parts[0].lower()} {parts[1].lower()}")
            else:
                raise InvalidInputError(f"Invalid order_by clause: '{clause.strip()}'")

        return ", ".join(validated_clauses)

    @classmethod
    async def get_all(cls: Type[T], order_by=None) -> List[T]:
        try:
            # If called from a specific subclass, use its table_name

View on GitHub (pinned to a7de90d38a)

Solutions

  1. Use only real, whitelisted field names from the model's allowed fields — check the model class for the permitted pattern
  2. Fix typos/case: fields are matched lowercased, so send lowercase snake_case names
  3. If exposing order_by via API, validate it against the model's field list before calling get_all

Example fix

// before
Note.get_all(order_by=request.query_params['sort'])
// after
allowed = {'created_at', 'updated_at', 'title'}
sort = request.query_params.get('sort', 'created_at')
if sort.lstrip('-').split(' ')[0] not in allowed:
    raise InvalidInputError(f'Invalid order_by field: {sort}')
Note.get_all(order_by=sort)
Defensive patterns

Strategy: validation

Validate before calling

allowed = get_allowed_order_fields(Note)  # from the model
raw = request.query_params.get('order_by', '')
for clause in raw.split(','):
    field = clause.strip().split()[0].lower()
    if field not in allowed:
        raise InvalidInputError(f'Invalid order_by field: {field}')

Type guard

def is_valid_order_by(order_by: str, allowed_fields: set[str]) -> bool:
    for clause in order_by.split(','):
        parts = clause.strip().lower().split()
        if not parts or parts[0] not in allowed_fields:
            return False
        if len(parts) == 2 and parts[1] not in ('asc', 'desc'):
            return False
        if len(parts) > 2:
            return False
    return True

Try / catch

try:
    items = await Model.get_all(order_by=raw)
except InvalidInputError:
    return JSONResponse(400, 'invalid order_by')

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of lfnovo/open-notebook@a7de90d38a (2026-08-27). Data as JSON: /api/errors/ee74663703b8de53. Report an issue: GitHub.