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

How to get the Windows 11 2026 Update

1 Share
Windows powers how people around the world work, learn, create, and connect every day. Today, we're announcing the availability of the Windows 11 2026 Update, also known as Windows 11, version 26H2. Built on the same servicing foundation as recent Windows 11 releases, version 26H2 delivers the latest Windows innovations with a streamlined update experience designed to minimize disruption and help you stay secure, productive, and up to date. As organizations and individuals continue to expect faster deployments and less downtime, Windows 11, version 26H2 takes advantage of the same modern servicing technologies that have simplified recent feature updates. For many eligible devices, moving to version 26H2 is as simple as installing a small enablement package, allowing people to start using this new release with minimal download size, quick installation, and a single restart.

A predictable and low-disruption update

Windows 11, version 26H2 is delivered as an enablement package for eligible devices running Windows 11, versions 25H2 and 24H2. An enablement package is a lightweight update that activates new features already included in the operating system through previous monthly servicing updates. Because versions 26H2, 25H2, and 24H2 share a common servicing branch and underlying codebase, many of the components required for version 26H2 are already present on devices before the feature update becomes available. This approach enables a significantly faster update experience compared to a traditional operating system upgrade. For eligible devices, installation is similar to a monthly update, requiring only a small download and a quick restart to transition to Windows 11, version 26H2. As with every Windows release, version 26H2 includes the latest quality, reliability, and security improvements designed to help protect people and organizations while delivering a dependable Windows experience.

How to get it

Windows 11, version 26H2 is now available for eligible devices running Windows 11, versions 25H2 and 24H2. To be among the first to receive the update, go to Settings > Windows Update and turn on Get the latest updates as soon as they're available. Once your device is eligible, you'll see the option to download and install Windows 11, version 26H2. As always, we take a measured and data-driven approach to broad availability and will be releasing version 26H2 using a controlled feature rollout. During rollout, we might place safeguard holds on devices where we detect a known compatibility issue that could impact the update experience. We will continue expanding availability over time and provide updates through the Windows release health hub.

Information for commercial and education customers

Windows 11, version 26H2 is available now through the tools organizations use to manage Windows updates, including Windows Autopatch and the Microsoft 365 admin center[i]. Existing management tools, update rings, policies, and deployment practices can continue to be used. We recommend that commercial and education customers begin targeted deployments to validate applications, devices, and business-critical workflows before expanding deployment more broadly throughout their organizations. Windows 11, version 26H2 continues the annual Windows feature update cadence, with a new Windows 11 feature update released during the second half of the calendar year. As with previous annual releases, installing version 26H2 resets the support lifecycle for supported editions of Windows 11. That means 24 months of support for Home and Pro editions and 36 months of support for Enterprise and Education editions. Select features and enhancements initially released as disabled by default in version 25H2 will be enabled by default in version 26H2. You can find more information on these features, plus guidance to help with planning and deployment, in the Windows IT Pro Blog.

Staying protected and productive

We recommend updating eligible devices to Windows 11, version 26H2 to take advantage of the latest Windows innovations, quality improvements, and security enhancements. We'll continue to closely monitor the rollout and share information about known issues, safeguards, and rollout progress through the Windows release health hub and official Windows communications channels. Thank you for your feedback and partnership as we continue to improve Windows for people and organizations around the world. We love to hear about your experience, so please keep your comments and suggestions coming in via Feedback Hub.
[i] Downloads in the Microsoft 365 admin center and similar channels may be delayed. 
Read the whole story
alvinashcraft
just a second ago
reply
Pennsylvania, USA
Share this story
Delete

Is it MSIX? Part 1: How to Tell if a Process Is Packaged

1 Share

Overheard in a recent conversation:

Enterprise Administrator: “The ***redacted*** application is awesome, but it really should be an MSIX package.”
Microsoft Developer: “Ummm, it already is.”

That sparked another question: How can you tell if an application is packaged or unpackaged?

Turns out there’s a few simple ways to check.

The key difference between packaged and unpackaged processes is whether they have package identity. Task Manager provides a convenient way to quickly see this:

  1. Run taskmgr.exe
  2. On the left, select Details
  3. Right-click a column heading and select Select columns
  4. Select Package name
  5. Select OK

The list of processes now shows Package name:

  • If the process is unpackaged, this field is blank.
  • If the process is packaged, this field shows the package’s Display Name1

For example:

Task Manager

In this example, AzVpnAppx.exe is a packaged process with the display name Azure VPN Client, whereas audiodg.exe has no Package name, so it’s unpackaged.

We also see AzVpnSysTrayApp.exe is a packaged process with the same display name as AzVpnAppx.exe. Display names are localized strings meant for human consumption, and thus aren’t guaranteed to be unique. They can be whatever the developer chooses to show.


1 If Task Manager can’t resolve the package’s localized display name, the Package name column may instead show an ms-resource: value based on the DisplayName declared in AppxManifest.xml. Windows uses this resource reference to find the appropriate localized string rather than displaying the resource reference to the user.

The post Is it MSIX? Part 1: How to Tell if a Process Is Packaged appeared first on Inside MSIX.

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

Introducing Shiny.AppFunctions — One C# Declaration for Siri and Gemini

1 Share

Shiny.AppFunctions is a new package that lets the platform assistants call into your .NET app. You declare a function once, in C#, and it becomes:

  • an App Intent on iOS 16+, available to Siri, Spotlight, the Shortcuts app and Apple Intelligence;
  • an Android AppFunction on Android 16+, available to Gemini and other agents the system allows.

It’s coming in Shiny 5.8.

NuGet package Shiny.AppFunctions

Two assistants, two very different APIs

Apple and Google both want assistants to act inside apps, and both have shipped a way for apps to say what they can do. The two share nothing:

iOS Android
Framework App Intents AppFunctions (Android 16 / API 36)
Language Swift, with compile-time metadata extraction Kotlin, with an XML schema and a bound service
Picking an app object as a parameter AppEntity + EntityQuery not supported — agents send ids
Voice phrases AppShortcutsProvider —
Callers Siri, Spotlight, Shortcuts, Apple Intelligence Gemini and allowlisted agents

For a .NET developer, that’s two native languages, two build pipelines and two ways to describe the same “create an order” operation. They drift apart the moment one of them changes.

With Shiny.AppFunctions, the operation is a C# record and the handler is a C# class. The id, the parameters, their descriptions and the business logic are shared, and each platform gets its own native form generated from them.

Setup

MauiProgram.cs
builder
.UseMauiApp<App>()
.UseShiny();
builder.Services.AddAppFunctions(); // source-generated

That’s all the registration there is. AddAppFunctions() is written by the package’s source generator, and it registers every handler, entity query and delegate in your app.

Declaring a function

A function is a record marked [AppFunction] that implements IAppFunction<TResult>. Its properties are the parameters, and one handler runs it:

using Shiny.AppFunctions;
public enum Priority { Low, Normal, Urgent }
public record OrderResult(string Number, int Quantity, double Total);
[AppFunction("create_order", Description = "Creates an order for a customer")]
[AppShortcut("Create an order in ${applicationName}", ShortTitle = "New Order", SystemImage = "cart.badge.plus")]
public record CreateOrder(
[property: AppParameter(Title = "Customer", Description = "Who the order is for")] Customer Customer,
int Quantity,
Priority Priority,
string? Note
) : IAppFunction<OrderResult>;
public class CreateOrderHandler(OrderStore store) : IAppFunctionHandler<CreateOrder, OrderResult>
{
public Task<OrderResult> Handle(CreateOrder request, AppFunctionContext context, CancellationToken cancellationToken)
{
if (request.Quantity is < 1 or > 1000)
throw new AppFunctionException(AppFunctionErrorCode.InvalidArgument, "Quantity must be between 1 and 1000");
var order = store.Create(request.Customer, request.Quantity, request.Priority, request.Note);
context.Say($"Order {order.Number} is in.");
return Task.FromResult(new OrderResult(order.Number, order.Quantity, order.Total));
}
}

That one declaration is everything both assistants need:

  • The id (create_order) is the same on both platforms. It’s the name a saved Shortcut and an agent refer to.
  • The description is what Siri, Apple Intelligence and Gemini use to decide when to call the function.
  • The parameters come from the record. Constructor parameters are required unless nullable, so Note is optional everywhere. Priority becomes an AppEnum on iOS and an enumerated value in the Android schema.
  • context.Say(...) is what Siri shows or speaks. On Android it comes back with the result, in a response extra.
  • AppFunctionException fails the call with a code that maps to each platform’s own error. The message is shown or spoken to the user, so it’s written for them.

The handler is resolved from a new DI scope for every call, so it takes your services like anything else in the app.

Entities — letting the assistant pick a customer

An entity is an app object that can be a parameter: a customer, a playlist, a project. Mark a record with [AppEntity] and give it a query:

[AppEntity("customer", Title = "Customer")]
public record Customer(string Id, string Name, string City);
public class CustomerQuery(ICustomers customers) : IAppEntityQuery<Customer>
{
public Task<IReadOnlyList<Customer>> GetByIds(IReadOnlyList<string> ids, CancellationToken ct) => customers.ByIds(ids, ct);
public Task<IReadOnlyList<Customer>> Search(string text, CancellationToken ct) => customers.Search(text, ct);
public Task<IReadOnlyList<Customer>> Suggested(CancellationToken ct) => customers.Recent(ct);
}

This is where the two platforms differ most, and where one declaration pays off:

  • On iOS it’s a real App Entity. Siri and Shortcuts show a picker: Suggested before the user types, Search while they type, and GetByIds to load the one they chose.
  • On Android there are no entity queries, so agents send an id. Shiny generates a companion search_customer function from the same query, returning [{ id, title }], so Gemini can find the customer first and then call create_order with its id.

Either way, the id is resolved back to a Customer through your query before the handler runs. An id the query doesn’t know fails the call with NotFound.

Siri phrases with no setup

[AppShortcut] turns a function into an App Shortcut. It’s available to Siri and Spotlight as soon as the app is installed, and the user doesn’t have to set anything up:

[AppFunction("count_open_orders", Title = "Open Orders", Description = "Counts the orders that have not shipped")]
[AppShortcut("How many orders are open in ${applicationName}", SystemImage = "shippingbox")]
[AppShortcut("Open orders in ${applicationName}")]
public record CountOpenOrders : IAppFunction<int>;

Every phrase must include ${applicationName}, and iOS allows up to 10 App Shortcuts per app. Both are checked at compile time. The attribute is ignored on Android, where Gemini works from the function’s description instead.

Gating every call with delegates

An assistant can call your app when the user isn’t signed in, or when a feature is switched off. An IAppFunctionDelegate sees every call before and after the handler:

public class SignInDelegate(IAuth auth) : IAppFunctionDelegate
{
public Task<AppFunctionGate> OnInvoking(AppFunctionContext context, CancellationToken ct)
=> Task.FromResult(context.FunctionId == "cancel_order" && !auth.IsSignedIn
? AppFunctionGate.OpenApp("Sign in to cancel orders.")
: AppFunctionGate.Allow);
}

AppFunctionGate.Deny(message) refuses the call. AppFunctionGate.OpenApp(message) asks the user to continue in the app on iOS, and then runs the call again in the foreground; on Android it refuses with the message. OnInvoked receives the result or the exception, which makes it the place for logging and telemetry. context.Platform tells you whether Siri or Gemini made the call. Delegates are found and registered by the generator.

What you don’t write

The source generator and the build step ship inside the package:

  • Generated C#: AddAppFunctions(), the function descriptors, and reflection-free argument binding, dispatch and result writing. It’s trim- and AOT-safe.
  • iOS: the Swift App Intents, App Entities, entity queries and AppShortcutsProvider, compiled with swiftc into the app. Apple’s App Intents metadata and Siri phrase processors run before signing.
  • Android: the AppFunctions schema, added to the app as assets. The AppFunctionService arrives through the manifest merger.

There’s no Swift, no Kotlin, no Info.plist entry, no entitlement and no AndroidManifest entry to write. The iOS build needs Xcode, since it runs swiftc and Apple’s processors.

The generator also checks your declarations: a missing handler, an unsupported parameter type, a duplicate id or a Siri phrase without ${applicationName} is a build error (SHAF001–SHAF013), not a failure at runtime.

The same functions, in your app

AppFunctionDispatcher runs the same pipeline Siri and Gemini use — new scope, binding, delegates, handler, JSON result — from inside the app:

var outcome = await dispatcher.Execute(
new AppFunctionInvocation("create_order"),
"""{"customer":"acme","quantity":2,"priority":"Normal"}""",
CancellationToken.None
);

Every function in dispatcher.Registry.Functions exposes GetParametersJsonSchema(). That’s enough to hand your app’s functions to an in-app AI chat as tools, or to an MCP server, without declaring them a third time.

Trying it

On an Android 16+ emulator, the shell can list and call your functions, the same way an agent does:

adb shell cmd app_function list-app-functions --package com.mycompany.myapp
adb shell "cmd app_function execute-app-function --package com.mycompany.myapp \
--function create_order --parameters '{\"customer\":\"acme\",\"quantity\":2,\"priority\":\"Normal\"}'"

On iOS, install the app and search for a shortcut’s title in Spotlight (“New Order”), or find the app’s actions in the Shortcuts app.

Things to know

  • Platforms: iOS 16+ and Android 16 (API 36)+. Below Android 16 the package does nothing. There’s no Mac Catalyst, macOS or Windows bridge; net10.0 has the contracts and the dispatcher.
  • Declare functions in the app project. The generator scans only the app, not class libraries. The services, enums and result types they use can live anywhere.
  • Calls can be cold starts with no UI. Persist what a handler changes, and don’t touch pages from it.
  • Parameters are string, int, long, double, bool, DateTimeOffset, enums and entities. Results can also be records and lists.
  • Titles and descriptions are not localized yet.

Get started

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

How do you deal with people who strongly disagree with your views?

1 Share
 
1. Begin by genuinely listening to them. Make sure that you understand them. It is often useful to ask for confirmation: restate what you think they are saying and make sure that you understand. Physically, it helps to turn your shoulders so that you face them. Don’t look at your screen while they talk.
 
2. Ask more questions. You can often effortlessly weaken the stance of someone who has not given much thought to an issue just by asking them questions. Don’t ‘challenge them’. Ask genuine questions. The objective is not to break their views with questions, but to establish the playing ground. Many people are unclear about what they think they believe. They may simply be repeated what they heard.
 
3. Focus on what you share in common. Often, the disagreement is smaller than you think. Furthermore, you can sometimes entirely avoid a conflict by establishing strong agreement on many points.
 
4. Unless you are clearly a knowledgeable person, do not immediately express your opinion. Begin by establishing your credibility. Refer to hard facts, data. Refer to people or documents you have consulted. This, in turn, should have been done ahead of time. Never engage in a debate without having done at least a bit of research and reflection.
 
5. Use language that fits the issue. You need to learn the vocabulary. Learn the technical terms and use them, although you may want to remain accessible, so explain them as well.
 
6. Tell stories. People understand by emotions as much as by their reason. Ideally, the story should be true and it should be backed by hard evidence.
 
7. Avoid personal attacks and do not tolerate them against you and others. If someone is insulting you, if it is fair to say so. You may say that you feel offended. It is the one case where you can and maybe should go on the offensive. Don’t answer by an insult, but attack the fact that they insulted you. You may ask for an apology.
Read the whole story
alvinashcraft
25 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Multiplayer Game Development with TypeScript, Phaser, Socket.io, and MongoDB

1 Share

Game data rarely looks the same for very long. A player document might start out with just a username and a score, and a few sprints later, it needs an inventory, a set of unlocked achievements, and a friends list, each shaped differently from the last. MongoDB's document model is a natural fit for that kind of growth, since a new field is just a new field, with no migration to write and no join to add. A match record can embed both players directly, scores and all, so reading back a finished match is a single document lookup rather than a query spread across multiple tables. Atomic operators like $inc and $max also mean stats and high scores can be updated safely by many concurrent players at once, without the application code having to coordinate a read-then-write itself.

In this tutorial, we're going to build a simple multiplayer game. Each player controls a white square on a canvas, and for thirty seconds, yellow coin squares spawn at random positions. Whoever collects the most coins when the timer runs out wins. Movement, coin spawning, and scoring all happen over WebSockets through Socket.io, with the server acting as the single source of truth for everything that happens during a match. MongoDB sits underneath that, storing player accounts, match records, and the leaderboard.

We'll use TypeScript, Zod, Socket.io, and Express on the backend, and Phaser, TypeScript, and Socket.io on the frontend. The MongoDB Node.js driver is used directly, without an ODM like Mongoose, so every query in this tutorial is plain driver code. If you'd rather skip ahead or just want to run the finished project, the full source is available on GitHub.

The post Multiplayer Game Development with TypeScript, Phaser, Socket.io, and MongoDB appeared first on DEV.



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

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
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories