| | 866 | |
| | 867 | == SQL Injection Prevention |
| | 868 | To reduce the risk of SQL injection, ChapterX uses Entity Framework Core as its ORM instead of writing raw SQL queries. EF Core automatically generates parameterized queries under the hood, user-supplied values are never concatenated directly into query strings. |
| | 869 | |
| | 870 | Because all database access goes through EF Core's API, parameterization is handled implicitly: |
| | 871 | {{{ |
| | 872 | public async Task<T?> GetByIdAsync(int id, CancellationToken cancellationToken = default) |
| | 873 | => await _dbSet.FindAsync([id], cancellationToken); |
| | 874 | |
| | 875 | public async Task AddAsync(T entity, CancellationToken cancellationToken = default) |
| | 876 | { |
| | 877 | await _dbSet.AddAsync(entity, cancellationToken); |
| | 878 | await _context.SaveChangesAsync(cancellationToken); |
| | 879 | } |
| | 880 | }}} |
| | 881 | Custom queries in repositories follow the same pattern — LINQ expressions are translated by EF Core into parameterized SQL, so user input is always treated as a value, never as executable SQL: |
| | 882 | {{{ |
| | 883 | public async Task<User?> GetByEmailAsync(string email, CancellationToken cancellationToken = default) |
| | 884 | => await _dbSet.FirstOrDefaultAsync(u => u.Email == email, cancellationToken); |
| | 885 | public async Task<IEnumerable<Chapter>> GetByStoryIdAsync(int storyId, CancellationToken cancellationToken = default) |
| | 886 | => await _dbSet.Where(c => c.StoryId == storyId).ToListAsync(cancellationToken); |
| | 887 | }}} |