Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
161969 stories
·
33 followers

How ORMs Still Let SQL Injection Through (and How to Close the Gaps)

1 Share

A lot of developers assume that once they're on an ORM, SQL injection stops being their problem. But it doesn't disappear. It just relocates.

ORMs like Sequelize, Prisma, TypeORM, and Knex parameterize the queries they build for you automatically. That part works.

The problem shows up in the parts of your app where you step outside that safe path: a raw query the ORM's query builder can't express cleanly, a dynamic sort column, or a single helper function reached for without a second thought.

Those are exactly the places attackers go looking first, and they're easy for developers to miss precisely because the rest of the codebase feels protected.

Four of those patterns are the focus here. Each one gets a working example that triggers the bug, a breakdown of why it's exploitable, and a rewritten version that closes it. I also include notes on exactly what moved and why.

You don't need a security background to follow along. You just need enough comfort with Node.js, SQL, and at least one ORM to read the code.

Prerequisites

The examples assume you're comfortable with:

  • The basics of Node.js and Express

  • Writing basic SQL queries

  • Basic use of an ORM (all code examples use Sequelize, but the patterns apply to Prisma, TypeORM, Knex, and others too)

  • How an HTTP request/response cycle works

Note on code examples: Throughout this guide, sequelize is assumed to already be an initialized Sequelize instance, with QueryTypes and Op imported from 'sequelize' alongside it, and User/Product are Sequelize models defined elsewhere in the app. Examples use Sequelize v6+ syntax (the current major version) since some of the APIs referenced here changed between major versions. You can adapt the syntax to the version and ORM you use.

Table of Contents

1. Raw Query Escape Hatches

Every major ORM ships an "escape hatch" for queries the query builder can't express cleanly: sequelize.query(), Prisma's $queryRawUnsafe, or TypeORM's query(). They exist for good reasons, like complex joins, window functions, and vendor-specific SQL.

The trouble starts when developers treat that escape hatch like the rest of the ORM, and interpolate user input directly into the string it builds.

How to Identify This Vulnerability in Your Code

Here's a typical filtered report endpoint:

// Express.js - Vulnerable
app.get('/reports', async (req, res) => {
  const { region } = req.query;
  const results = await sequelize.query(
    `SELECT * FROM sales WHERE region = '${region}'`,
    { type: QueryTypes.SELECT }
  );
  res.json(results);
});

The query builder never sees this string. It's handed straight to the database driver exactly as written.

Why This Matters

A request like ?region=' OR '1'='1 turns the query into SELECT * FROM sales WHERE region = '' OR '1'='1', which is always true. Every row in the table comes back, regardless of region.

Worse, because this code lives inside an ORM-based project, it often doesn't get the same scrutiny a raw mysql.query() call would in a non-ORM codebase. Reviewers assume the ORM already handled it.

How to Fix This Vulnerability

Pass values through the replacement/binding mechanism the raw-query API already provides, instead of building the string yourself:

// Express.js - Secure
app.get('/reports', async (req, res) => {
  const { region } = req.query;
  const results = await sequelize.query(
    'SELECT * FROM sales WHERE region = :region',
    {
      replacements: { region },
      type: QueryTypes.SELECT
    }
  );
  res.json(results);
});

The fix isn't avoiding raw queries entirely, because sometimes you genuinely need them. It's never building the SQL string by hand when a binding mechanism is sitting right there.

Never concatenate user input into a raw query string. Use the replacement/binding API your raw-query method already provides.

2. Identifiers That Can't Be Parameterized

Parameterized queries protect values. They don't protect identifiers like table names, column names, or ORDER BY direction. Those have to be part of the SQL string itself.

That's why a dynamic sort feature is one of the most common places injection sneaks back into otherwise well-written ORM code.

How to Identify This Vulnerability in Your Code

Here's a typical sortable list endpoint:

// Express.js - Vulnerable
app.get('/users', async (req, res) => {
  const { sortBy = 'created_at' } = req.query;
  const users = await sequelize.query(
    `SELECT * FROM users ORDER BY ${sortBy}`,
    { type: QueryTypes.SELECT }
  );
  res.json(users);
});

sortBy goes straight from the query string into the ORDER BY clause, with nothing in between.

Why This Matters

sortBy looks like a harmless UI convenience until someone sends created_at; DROP TABLE users; --. Whether that exact payload runs depends on your database driver: Postgres's pg driver executes stacked statements like this by default, while MySQL's mysql2 blocks them unless multipleStatements: true is explicitly set.

Either way, the underlying problem is the same: you have arbitrary SQL sitting in a position that was only ever meant to hold a column name.

How to Fix This Vulnerability

Since identifiers can't be bound as parameters, the only safe option is an allowlist:

// Express.js - Secure
const ALLOWED_SORT_COLUMNS = ['created_at', 'name', 'email'];

app.get('/users', async (req, res) => {
  const { sortBy = 'created_at' } = req.query;
  const column = ALLOWED_SORT_COLUMNS.includes(sortBy) ? sortBy : 'created_at';

  const users = await sequelize.query(
    `SELECT * FROM users ORDER BY ${column}`,
    { type: QueryTypes.SELECT }
  );
  res.json(users);
});

Never pass user input through to an identifier position, even after "sanitizing" it first. Sanitization for identifiers is much easier to get wrong than for values.

Identifiers can't be parameterized. If user input decides a column or table name, allowlist it, don't sanitize it.

3. Second-Order Injection Through Stored Data

This one catches teams off guard because the input was parameterized...the first time it was written to the database.

The injection happens later, when that already-stored value gets reused inside a different, unparameterized query.

How to Identify This Vulnerability in Your Code

Here's a registration flow, and elsewhere in the codebase, an admin search feature:

// Express.js - Vulnerable

// Step 1: user registration, correctly parameterized
app.post('/register', async (req, res) => {
  await User.create({ username: req.body.username });
  res.json({ success: true });
});

// Step 2: somewhere else in the codebase, an admin search feature
app.get('/admin/search', async (req, res) => {
  const user = await User.findByPk(req.params.id);
  const results = await sequelize.query(
    `SELECT * FROM audit_log WHERE actor = '${user.username}'`,
    { type: QueryTypes.SELECT }
  );
  res.json(results);
});

Step 1 is completely safe on its own. Step 2 is where things go wrong.

Why This Matters

A username like admin' OR '1'='1 passes through step 1 without any issue. Sequelize's create() parameterizes it, so it's stored exactly as typed. It sits in the database looking completely normal.

The payload only fires when step 2 pulls that value back out and drops it straight into a raw query string. "It's already in our own database" is not the same thing as "it's safe". It's still attacker-controlled if it originated from user input anywhere upstream.

How to Fix This Vulnerability

Same fix as pattern #1. You bind the value instead of concatenating it:

// Express.js - Secure

// Step 1 (registration) is unchanged — it was already parameterized

app.get('/admin/search', async (req, res) => {
  const user = await User.findByPk(req.params.id);
  const results = await sequelize.query(
    'SELECT * FROM audit_log WHERE actor = :actor',
    {
      replacements: { actor: user.username },
      type: QueryTypes.SELECT
    }
  );
  res.json(results);
});

The bug here is harder to spot than pattern #1, because the tainted data crosses a database round-trip before it becomes dangerous.

Data from your own database isn't automatically safe. If it originated from user input anywhere upstream, it still needs to be parameterized everywhere it's used.

4. Raw SQL Smuggled Into Normal ORM Calls

Pattern #1 covered the obvious raw-query escape hatch, a method you reach for on purpose when you need real SQL. This one is sneakier: injection hiding inside a call that looks completely ORM-managed. It's not raw SQL, right? It's just one helper function among many.

How to Identify This Vulnerability in Your Code

Here's a typical filtered product listing:

// Express.js - Vulnerable
app.get('/products', async (req, res) => {
  const { minPrice } = req.query;
  const products = await Product.findAll({
    where: sequelize.literal(`price > ${minPrice}`)
  });
  res.json(products);
});

Product.findAll() looks like a fully safe, parameterized ORM call...right up until sequelize.literal() shows up inside it.

Why This Matters

literal() tells Sequelize "don't touch this, insert it into the SQL exactly as written." Anything interpolated into it is exactly as injectable as pattern #1, just wearing a normal-looking ORM method as a disguise.

Send ?minPrice=0 OR 1=1 and the WHERE clause becomes unconditionally true: every row comes back, price filter or not. No stacked statement needed this time. It's a single boolean expression, so it works the same regardless of database driver.

How to Fix This Vulnerability

Stop reaching for literal(). Sequelize's own operator API already covers this case:

// Express.js - Secure
app.get('/products', async (req, res) => {
  const minPrice = Number(req.query.minPrice);

  if (!Number.isFinite(minPrice)) {
    return res.status(400).json({ error: 'minPrice must be a number' });
  }

  const products = await Product.findAll({
    where: { price: { [Op.gt]: minPrice } }
  });
  res.json(products);
});

Op.gt, Op.between, Op.in, and the rest of Sequelize's operator API already cover the vast majority of cases people reach for literal() for, and they parameterize correctly by default. Validating that minPrice is actually a number, before it ever reaches the query, closes the gap even if a future refactor reintroduces literal() somewhere else.

Grep for literal(, fn(, and your ORM's other raw escape hatches everywhere they appear in the codebase, not just inside the queries that are clearly "raw."

Summary

For a quick recap, here's how the four patterns line up:

Pattern Root Cause Core Fix
Raw Query Escape Hatches User input concatenated into a raw query string Bind values via the replacement/parameter API
Identifier Injection Column/table/sort input can't be parameterized Allowlist allowed identifiers explicitly
Second-Order Injection Stored data trusted because it was parameterized once Parameterize every query, including ones using your own stored data
Literal Injection Raw SQL smuggled into an otherwise ORM-managed call Avoid literal()/raw(), use the ORM's operator API instead

None of these four bugs takes a skilled attacker to find. Each one is just a value that never made it into the ORM's parameterization path.

What's worth making part of your workflow: parameterize values, allowlist identifiers, treat data pulled from your own database as no safer than a fresh request, and treat literal()/raw() calls as exactly as dangerous as a dedicated raw-query method (no matter how safe the surrounding code looks).

Fixing these gaps is only half the picture. Knowing how someone actually goes looking for them (and chains them together during a real assessment) is the half most developers never see.

If you're curious about that offensive perspective, this deep dive on SQL injection through ORMs walks through exactly how these four patterns get exploited step by step in a live penetration test.



Read the whole story
alvinashcraft
just a second ago
reply
Pennsylvania, USA
Share this story
Delete

AI Is Writing Code Faster Than Humans Can Review It. Using Another AI to Check It Isn’t the Answer

1 Share

AI has changed the economics of writing software.

A developer can describe a feature, ask an AI coding assistant to implement it, generate unit tests, fix compilation errors, refactor the result, and move on to the next task faster than ever before.

According to GitHub’s research into AI-assisted development, developers using Copilot completed a coding task significantly faster than developers without it. GitHub research on developer productivity with Copilot

That’s a productivity breakthrough.

But there is a problem hiding inside that productivity gain:

AI can now generate code faster than humans can realistically review it.

I recently discussed this with a developer who put the problem very simply: there is too much code to read. Developers don’t have time to go through everything AI generates, so increasingly they have to trust it.

And when they do want a review?

They ask another AI.

That should make us uncomfortable.

AI Solved the Coding Bottleneck. It Didn’t Solve the Trust Bottleneck.

For decades, software development had a natural constraint: writing code took time.

A developer designed something, wrote the code, looked at it, debugged it, wrote unit tests, discussed it during code review and eventually shipped it.

There were plenty of shortcuts and plenty of bad software, but the human developer was intimately involved in producing the code.

Generative AI changes that relationship.

Today, a developer might generate hundreds of lines of code in minutes. AI coding agents are pushing this further by taking on increasingly complex development tasks.

But our ability to understand code hasn’t suddenly increased by 10x.

The result is a strange imbalance:

We have dramatically increased our ability to produce software without dramatically increasing our ability to verify it.

That may become one of the biggest challenges of AI-assisted software development.

So We Ask Another AI

There is an obvious solution.

If one AI can generate code faster than I can read it, perhaps another AI can review it faster than I can read it too.

We’re already moving toward this pattern.

AI writes the code → AI reviews the pull request → AI generates the unit tests → AI reviews the tests → AI finds a problem → AI fixes it → everything turns green.

But what have we actually established?

We have created agreement between machines.

That isn’t necessarily the same thing as verification.

AI code review can be extremely useful. We use AI ourselves, and ignoring the productivity benefits would make little sense.

The problem begins when AI reviewing AI becomes a substitute for evidence that the software actually behaves correctly.

An AI Saying “Looks Good” Is Still an Opinion

Imagine a developer submits a pull request containing 2,000 lines of AI-generated code.

The developer doesn’t have time to inspect every line.

An AI reviewer analyzes it and reports that there are no obvious bugs, the error handling looks good, the tests appear appropriate and the code is ready to merge.

That’s useful information.

But it isn’t proof that the application behaves correctly under the conditions that matter.

And the same problem appears with tests.

AI can generate a large unit test suite remarkably quickly. It can generate mocks, assertions, edge cases and test data.

Then another AI can look at those tests and tell you they appear comprehensive.

Meanwhile, you may have tests that pass without meaningfully testing different behavior, tests that access files or networks, or tests whose mocking makes them far less useful than they appear.

We’ve written previously about why duplicate unit tests can give teams misleading confidence and why unit tests shouldn’t depend on files, networks or the registry.

You can end up with:

200 tests.
85% code coverage.
Everything green.
Two AIs saying everything looks fine.

And still have surprisingly little reason to trust the change.

More AI Requires More Verification, Not Less

The answer isn’t to stop using AI.

That train has left the station and is currently generating a pull request. 🤖

The answer is to recognize that generation and verification are different problems.

As AI increases the amount of software we can produce, we need verification techniques that scale with it.

Unit testing is part of that. Static analysis is part of that. Code coverage, CI/CD checks, integration testing and system testing are all part of it.

And increasingly, we also need to verify the quality of the tests themselves.

This distinction matters.

An AI can tell you that a test looks useful.

A verification tool can run the test and examine what actually happens.

Those are fundamentally different kinds of evidence.

This Is Why We Built Test Review

At Typemock, we’ve spent years working on unit testing and mocking with Typemock Isolator, including making difficult .NET and legacy code testable without requiring developers to redesign production code just to mock it.

With AI, we’re seeing the problem change.

Writing tests used to be expensive.

Now generating tests is becoming cheap.

Trusting those tests is becoming expensive.

That’s one of the reasons we created Typemock Test Review.

Test Review isn’t another AI reading your AI-generated test and giving it a thumbs-up.

It runs the tests and looks for problems in their actual behavior, including duplicate tests, dependencies on external resources and problematic fakes.

We’ve discussed this shift before in The Next Evolution of Unit Testing Isn’t Better Test Generation, It’s Test Validation.

But the larger AI code review problem makes the reason increasingly clear.

The objective isn’t to give developers another 200 things to review.

It’s the opposite.

If AI generates 200 tests, the developer shouldn’t have to manually inspect 200 tests to determine whether they’re valuable.

We need tools that help developers focus their limited attention on the tests and code that actually deserve it.

The Next AI Coding Problem Is Trust

AI coding tools are going to get better.

They’ll generate more code, create larger changes, fix more bugs and increasingly operate as autonomous agents.

That makes the trust problem more important, not less.

Because eventually:

“I read every line.”

may no longer be a realistic software development strategy.

But neither should:

“Another AI checked it.”

AI can generate the code.

AI can help review the code.

But before we trust it, we still need evidence that it works.

Try Test Review

See how Typemock Test Review helps identify unit tests that may be adding test count without adding confidence.

Learn more about Typemock Test Review →

The post AI Is Writing Code Faster Than Humans Can Review It. Using Another AI to Check It Isn’t the Answer appeared first on Typemock.

Read the whole story
alvinashcraft
15 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

How to Augment Voice Calls with Twilio Intelligence in C#

1 Share
How to Augment Voice Calls with Twilio Intelligence in C#
Read the whole story
alvinashcraft
43 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Octopus Approvals

1 Share

Octopus Approvals is a lightweight approval workflow that lets users gate deployments to environments that are sensitive to change. For customers that can't justify the cost of a full ITSM implementation, it provides approval gating to ensure the right people review before a deployment progresses to the specified environments. This allows deployments to progress safely and satisfies standard compliance requirements.

:img{ src="/blog/img/octopus-approvals/approvals-main.png" alt="Home page for Octopus approvals showing two change requests waiting approval" loading="lazy" }

The current landscape

Manual interventions were initially built to provide the ability to pause a deployment at crucial points to let a human review the state before progressing. We've seen customers build a makeshift approval system by stitching together a couple of manual intervention steps. This works for basic use cases but not particularly well at scale and has enforcement gaps.

The next step up is to use ITSM providers like SNOW and JSM. These are perfect for those in highly regulated industries with complex requirements. If you have strict redeployment processes or need to configure a subset of approvers, then that is still the right solution for you; we are not trying to replace these providers.

Why we've built Octopus Approvals

Not everyone needs a full ITSM change management system, but many still want to know that a release has the buy-in of key stakeholders before it goes out the door. Octopus Approvals have been designed for customers occupying the middle ground, with more complex approval requirements than manual interventions can meet, but who can't yet justify the cost of a full ITSM implementation.

Our aim with Octopus Approvals is to bridge this chasm as your compliance requirements grow and help you reach the next level. Octopus Approvals occur before deployment begins; approval is not a step that can be skipped, and it won't take up a spot in your task queue until it has been approved. Within Approvals, you can explicitly set the parameters required to approve a deployment, including preventing the deployment creator from approving and specifying how many people must approve and from which team(s).

Octopus Approvals walk-through

I want to set up an approval rule requiring at least two team members to approve a deployment before it goes to production. This applies across all projects in my team's ownership, but only for production; I don't want to slow down deployments to non-production environments.

Approvals are set at a space level; a single approval rule can span multiple projects. I'm going to use tags to determine which projects and environments are affected, enabling a more dynamic selection. Any new projects tagged as owned by my team will automatically have this rule applied.

:img{ src="/blog/img/octopus-approvals/approval-create.png" alt="Screenshot showing how to create an approval rule using tag sets" loading="lazy" }

When a project owned by Fire & Motion has a deployment created for a production environment, the task will wait for all required approvals. Change requests can be reviewed and approved from the deployment task and the change request tab within the approvals page. Once all required approvals have been received, the deployment will continue.

Each release requires only one approval; redeployments of the same release to the same environment do not require another approval. This ensures that rollbacks to approved versions are not blocked.

For scheduled deployments, an approval will be available as soon as the deployment is created, so you can schedule a deployment outside working hours but approve it before you clock off. This ensures that deployments stay within the approved change windows, but the checks can happen when it's practical for the team.

:img{ src="/blog/img/octopus-approvals/approval-change-request.png" alt="Screenshot showing a change request awaiting review" loading="lazy" }

Rejected change requests

If any approver rejects the change request, the deployment will not proceed and will be marked as failed. You can not revive a rejected change request; you'll need to create a new deployment, which will create a new change request. You can use your normal process to identify failed deployments, or set up a Subscription to be notified of change request status.

Reviewing and approving change requests

Change requests can be viewed on the Approvals page. From here, you can see who approved the deployment and when, along with deployment details. Approvals and changes to rules can also be tracked via the audit page; there are 'Approval Rule' and 'Server Task Approval' document types that the audit page can be filtered by.

:img{ src="/blog/img/octopus-approvals/approval-complete.png" alt="Screenshot showing a complete change request" loading="lazy" }

Notifications

The same event types used to filter the audit page can also be used to set up targeted notifications. Using our subscriptions feature, you can subscribe to notifications about rule changes to help ensure your team remains compliant. Notifications can be sent via email, Slack, Teams or webhook. Note the below screenshot is the first iteration of our Slack integration, we are currently working on providing a more actionable message.

:img{ src="/blog/img/octopus-approvals/approval-notification.png" alt="Screenshot showing a Slack notification following a change to an Approval Rule" loading="lazy" }

We will be releasing a separate blog post detailing how you can use subscriptions and webhooks to receive more targeted notifications, so that users who need to approve the deployment are the ones who receive the notification. Subscribe to our newsletter to get this in your inbox.

Known limitations

  • Any of the required approvers are counted towards the total. If you specified two teams within the approvers, any two approvals across those teams will suffice.
  • This cannot be used to automate your approval process.
  • Approvals only work for deployments; this functionality is not available for runbooks.
  • Approvals, unfortunately, cannot be used to finally get your mother-in-law's approval.

Octopus Approvals is now available as a Public Preview for cloud customers and in the 2026.3 server release.

Read more about getting started with Approvals here.

Happy deployments!

Read the whole story
alvinashcraft
56 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Microsoft Agent Framework for .NET (part 4): static methods, instance methods, MCP Clients as custom Agent tools

1 Share
MAF Agents can have custom capabilities through C# methods or MCP clients. Let’s see how to define and use them.
Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

Build Offline Agent Loops for a Vonage Voice Agent

1 Share
Use stored call records to build a regression runner and transcript review loop that find patterns, propose eval cases, and help you improve a voice agent without breaking what already works.
Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories