Skip to content
Report library
Purpose / Data analysis

Nodejs Backend Patterns Skill Security Audit

What the author says it does (original text)

Build production-ready Node.js backend services with Express/Fastify, implementing middleware patterns, error handling, authentication, database integration, and API design best practices. Use when creating Node.js servers, REST APIs, GraphQL backends, or microservices architectures.

Independent security check

Do not install or run it yet

Files checked
3
Risks found
6
Could it run dangerous commands?Looks for programs run straight after downloading, remote control of your computer, and hidden commands.No risks found
Could it expose your files or keys?Looks for uploads of files containing passwords or keys, and keys written directly in the code.Risks found: 3
High risk

The update example inserts request-controlled field names into SQL, enabling injection or unauthorized column changes

Source references: 2
What we found

The controller passes the whole request body as updates, while the repository builds SQL identifiers from Object.keys(updates). Only values use placeholders; attacker-controlled field names are interpolated directly. A TypeScript DTO does not filter HTTP JSON at runtime.

Why this matters

If an attacker can call the update endpoint, a crafted field name could modify roles, permissions, or other disallowed columns, alter query structure, or repeatedly break the query. The operation runs with the application's database privileges.

This is a copyable documentation example, not code executed merely by reading the Skill. If adopted, however, the request body becomes `updates` with no shown runtime allowlist. The repository parameterizes values but interpolates `Object.keys(updates)` as SQL identifiers. A crafted key could alter SQL structure, while ordinary extra keys could enable unauthorized column updates. Users can ask for strict runtime schema validation and a fixed mapping of permitted update columns.

references/details.md:140In the instructionsOpen original file
  async updateUser(req: Request, res: Response, next: NextFunction) {    try {      const { id } = req.params;      const updates: UpdateUserDTO = req.body;      const user = await this.userService.updateUser(id, updates);      res.json(user);    } catch (error) {
Show 1 other places
references/details.md:261In the instructionsOpen original file
  async update(id: string, updates: UpdateUserDTO): Promise<UserEntity | null> {    const fields = Object.keys(updates);    const values = Object.values(updates);    const setClause = fields      .map((field, idx) => `${field} = $${idx + 2}`)      .join(", ");    const query = `      UPDATE users      SET ${setClause}, updated_at = CURRENT_TIMESTAMP      WHERE id = $1      RETURNING *    `;    const { rows } = await this.db.query(query, [id, ...values]);    return rows[0] || null;
Medium risk

Request logging captures the full URL, IP, and User-Agent, which can expose tokens or personal data in logs

Source references: 1
What we found

The logger directly records req.url, req.ip, and User-Agent. A full URL includes its query string; tokens, email addresses, searches, or reset codes placed in query parameters would therefore enter the logging system.

Why this matters

People with access to local logs, centralized logging, or support exports could obtain account tokens, identifiers, and activity data. Log retention may also exceed the lifetime of the original request.

This logging middleware is an example; the Skill does not itself collect data. If adopted, it logs `req.url`, IP, and User-Agent when each response finishes. Because `req.url` can contain the query string, reset codes, tokens, email addresses, or searches placed in URLs may enter logs. Impact depends on URL usage and log access/retention. Users can require path-only logging and removal or redaction of query parameters and identifiers.

references/details.md:457In the instructionsOpen original file
  // Log response when finished  res.on("finish", () => {    const duration = Date.now() - start;    logger.info({      method: req.method,      url: req.url,      status: res.statusCode,      duration: `${duration}ms`,      userAgent: req.headers["user-agent"],      ip: req.ip,    });  });
Medium risk

The generic cache decorator puts every argument in a Redis key and stores the complete result

Source references: 4
What we found

The cache key contains the method name plus JSON.stringify(args), and the full result is serialized into Redis. On login, user, payment, or token-related methods, passwords, tokens, email addresses, and response records could appear in both keys and values; keys are also commonly exposed to monitoring and diagnostics.

Why this matters

Redis administrators, backups, monitoring systems, or an attacker with cache access could read sensitive arguments and responses. Identical method names and argument keys across contexts may also cause inappropriate cache reuse.

The risk is conditional: the source does not show this decorator applied to login, payment, or user methods, but it is an unrestricted generic decorator. When used, it places the JSON representation of every argument in a Redis key and serializes the entire return value as the cached value. On sensitive methods this could expose passwords, tokens, or personal records and create cross-user cache mistakes. Users can require explicit exclusions, hashed tenant/user-scoped keys, and filtered cached results.

references/advanced-patterns.md:333In the instructionsOpen original file
  async set(key: string, value: any, ttl?: number): Promise<void> {    const serialized = JSON.stringify(value);    if (ttl) {      await redis.setex(key, ttl, serialized);    } else {      await redis.set(key, serialized);    }  }
Show 3 other places
references/advanced-patterns.md:363In the instructionsOpen original file
    descriptor.value = async function (...args: any[]) {      const cache = new CacheService();      const cacheKey = `${propertyKey}:${JSON.stringify(args)}`;      const cached = await cache.get(cacheKey);      if (cached) {        return cached;      }      const result = await originalMethod.apply(this, args);      await cache.set(cacheKey, result, ttl);
references/advanced-patterns.md:355In the instructionsOpen original file
// Cache decoratorexport function Cacheable(ttl: number = 300) {  return function (    target: any,    propertyKey: string,    descriptor: PropertyDescriptor,  ) {    const originalMethod = descriptor.value;    descriptor.value = async function (...args: any[]) {      const cache = new CacheService();      const cacheKey = `${propertyKey}:${JSON.stringify(args)}`;
references/advanced-patterns.md:367In the instructionsOpen original file
      const cached = await cache.get(cacheKey);      if (cached) {        return cached;      }      const result = await originalMethod.apply(this, args);      await cache.set(cacheKey, result, ttl);      return result;    };
Could it delete files or keep running?Looks for broad file deletion, disk overwrites, and programs set to start automatically.No risks found
Could it bypass safety checks?Looks for skipped website security checks, excessive file access, or actions that skip your approval.Risks found: 2
Medium risk

Seven-day refresh tokens have no rotation or revocation check, allowing continued access after theft

Source references: 2
What we found

refreshToken verifies only the JWT signature and that the user still exists, then issues a new access token. The refresh token lasts seven days, with no token version, server-side session record, revocation list, one-time rotation, or reuse detection.

Why this matters

After copying a refresh token, an attacker could keep obtaining new access tokens for its remaining lifetime even if the legitimate user logs out or changes their password, unless the signing key is replaced globally or the user is deleted.

The example refresh flow verifies the signature, checks that the user exists, and then issues a new access token. The shown method has no rotation, server-side session state, or revocation check. Refresh tokens are signed for seven days, so a stolen token could be reused until expiry unless controls exist outside this example. Users can require server-side sessions/token families, rotation on every refresh, reuse detection, and immediate revocation.

references/advanced-patterns.md:274In the instructionsOpen original file
  async refreshToken(refreshToken: string) {    try {      const payload = jwt.verify(        refreshToken,        process.env.REFRESH_TOKEN_SECRET!,      ) as { userId: string };      const user = await this.userRepository.findById(payload.userId);      if (!user) {        throw new UnauthorizedError("User not found");      }      const token = this.generateToken({        userId: user.id,        email: user.email,      });      return { token };    } catch (error) {      throw new UnauthorizedError("Invalid refresh token");    }  }
Show 1 other places
references/advanced-patterns.md:304In the instructionsOpen original file
  private generateRefreshToken(payload: any): string {    return jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET!, {      expiresIn: "7d",    });  }
Low risk

The Fastify example reflects any request origin instead of enforcing a production allowlist

Source references: 3
What we found

The example configures CORS with origin: true, which accepts and reflects the request Origin. This is not a strict allowlist despite the main file's warning against wildcard production CORS. If credentials are later enabled or browser-origin isolation is relied upon, the default expands permitted origins.

Why this matters

A malicious website could make cross-origin calls to the API from a victim's browser. If the deployment also permits cookies or other ambient credentials, the hostile page may read responses; without credentials, browser-origin restrictions on public endpoints are still removed.

This is a Fastify setup example and is not started automatically by the Skill. If copied, `origin: true` dynamically accepts/reflects request origins rather than enforcing a production allowlist. The snippet does not show credential support, so cross-site account access cannot be asserted; if credentials are later enabled or Origin is treated as a security boundary, arbitrary sites gain a broader browser access surface. Users can require an environment-specific origin allowlist and tests that reject unlisted origins.

references/details.md:58In the instructionsOpen original file
// Pluginsawait fastify.register(helmet);await fastify.register(cors, { origin: true });await fastify.register(compress);
Show 2 other places
SKILL.md:32In the instructionsOpen original file
5. **Implement logging**: Use structured logging (Pino, Winston)6. **Add rate limiting**: Prevent abuse7. **Use HTTPS**: Always in production8. **Implement CORS properly**: Don't use `*` in production9. **Use dependency injection**: Easier testing and maintenance
SKILL.md:33In the instructionsOpen original file
6. **Add rate limiting**: Prevent abuse7. **Use HTTPS**: Always in production8. **Implement CORS properly**: Don't use `*` in production9. **Use dependency injection**: Easier testing and maintenance
Could it mislead the AI or hide text?Checks the skill instructions for requests to ignore you, influence the report, or hide text in invisible characters.No risks found
Could it change links or payment recipients without asking?Looks for forced referral or payment changes combined with instructions to hide the change.Risks found: 1
High risk

The order example trusts supplied prices and quantities, risking underpayment or negative inventory

Source references: 3
What we found

createOrder accepts items, calculates the order total from them, stores item.price, and unconditionally subtracts item.quantity from stock. It does not obtain authoritative prices from the product table or require sufficient stock and positive quantities in the update.

Why this matters

If items originate from a customer request, a customer could submit discounted, zero, negative, or excessive values, corrupting revenue, order totals, and inventory. The transaction makes these operations atomic but does not make them commercially valid.

This is example code and becomes risky only if adopted with client-controlled `items`. The source passes `items` to an unseen `calculateTotal`, so validation of the total cannot be confirmed. However, `item.price` and `item.quantity` are directly supplied to SQL, and the stock update has no positive-quantity or sufficient-stock condition. An attacker could submit negative/excess quantities or forged line prices. Users can require server-side prices, positive-integer validation, and an atomic stock-availability check.

references/advanced-patterns.md:185In the instructionsOpen original file
export class OrderService {  constructor(private db: Pool) {}  async createOrder(userId: string, items: any[]) {    const client = await this.db.connect();    try {      await client.query("BEGIN");      // Create order      const orderResult = await client.query(        "INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING id",        [userId, calculateTotal(items)],      );      const orderId = orderResult.rows[0].id;
Show 2 other places
references/advanced-patterns.md:201In the instructionsOpen original file
      // Create order items      for (const item of items) {        await client.query(          "INSERT INTO order_items (order_id, product_id, quantity, price) VALUES ($1, $2, $3, $4)",          [orderId, item.productId, item.quantity, item.price],        );        // Update inventory        await client.query(          "UPDATE products SET stock = stock - $1 WHERE id = $2",          [item.quantity, item.productId],        );      }
references/advanced-patterns.md:208In the instructionsOpen original file
        // Update inventory        await client.query(          "UPDATE products SET stock = stock - $1 WHERE id = $2",          [item.quantity, item.productId],        );      }

Inside this skill

4 instruction sections

This Skill is a collection of Node.js backend guidance and copyable TypeScript examples; its main file directs the agent to two reference documents. The supplied source contains no installation command or auto-executing script, so the identified risks arise mainly if the examples are adopted.

View source
SKILL.md:21In the instructionsOpen original file
## Detailed patterns and worked examplesDetailed pattern documentation lives in `references/details.md`. Read that file when the navigation tier above is insufficient.
references/details.md:584In the instructionsOpen original file
Key patterns covered in [references/advanced-patterns.md](references/advanced-patterns.md):- **PostgreSQL with connection pool** — `pg` Pool configuration and graceful shutdown- **MongoDB with Mongoose** — connection management and schema definition- **Transaction pattern** — `BEGIN`/`COMMIT`/`ROLLBACK` with `pg` client## Authentication & AuthorizationJWT-based auth with access tokens (short-lived, 15m) and refresh tokens (7d). Full `AuthService` implementation with `bcrypt` password comparison in [references/advanced-patterns.md](references/advanced-patterns.md).

The examples connect to PostgreSQL, MongoDB, and Redis and read connection details and JWT secrets from environment variables. They do not send those values to a hard-coded third party, but adopted code would operate with the application process's existing database and cache privileges.

View source
references/advanced-patterns.md:49In the instructionsOpen original file
  () =>    new Pool({      host: process.env.DB_HOST,      port: parseInt(process.env.DB_PORT || "5432"),      database: process.env.DB_NAME,      user: process.env.DB_USER,      password: process.env.DB_PASSWORD,      max: 20,      idleTimeoutMillis: 30000,      connectionTimeoutMillis: 2000,    }),);
references/advanced-patterns.md:126In the instructionsOpen original file
const connectDB = async () => {  try {    await mongoose.connect(process.env.MONGODB_URI!, {      maxPoolSize: 10,      serverSelectionTimeoutMS: 5000,      socketTimeoutMS: 45000,    });
references/advanced-patterns.md:318In the instructionsOpen original file
const redis = new Redis({  host: process.env.REDIS_HOST,  port: parseInt(process.env.REDIS_PORT || "6379"),  retryStrategy: (times) => {    const delay = Math.min(times * 50, 2000);    return delay;  },});

The examples cover operations that create, update, delete, and transact on business data. These are documentation examples rather than evidence of executed actions; once copied into routes, their safety depends on caller authentication, field validation, and database permissions.

View source
references/details.md:140In the instructionsOpen original file
  async updateUser(req: Request, res: Response, next: NextFunction) {    try {      const { id } = req.params;      const updates: UpdateUserDTO = req.body;      const user = await this.userService.updateUser(id, updates);      res.json(user);    } catch (error) {      next(error);    }  }  async deleteUser(req: Request, res: Response, next: NextFunction) {    try {      const { id } = req.params;      await this.userService.deleteUser(id);      res.status(204).send();    } catch (error) {
references/advanced-patterns.md:191In the instructionsOpen original file
    try {      await client.query("BEGIN");      // Create order      const orderResult = await client.query(        "INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING id",        [userId, calculateTotal(items)],      );      const orderId = orderResult.rows[0].id;      // Create order items      for (const item of items) {        await client.query(          "INSERT INTO order_items (order_id, product_id, quantity, price) VALUES ($1, $2, $3, $4)",          [orderId, item.productId, item.quantity, item.price],        );        // Update inventory        await client.query(          "UPDATE products SET stock = stock - $1 WHERE id = $2",          [item.quantity, item.productId],        );      }      await client.query("COMMIT");      return orderId;    } catch (error) {      await client.query("ROLLBACK");      throw error;
Start here · InstructionsSKILL.md
nodejs-backend-patterns
Lines connect the instruction file to its sections, not an observed execution order. Select a section to read the source.

File reference map

References: 2
Files making referencesReferenced content
Lines show actual file references, not execution order. Select a node to highlight its connections and inspect the files and source locations. Dashed lines include files that still need locating.
Files and check records3 files

Coverage and gaps

Content covered in each file

These are the source ranges included in this check, not a guarantee that every issue has been resolved.

  • SKILL.mdFull text included
  • references/details.mdFull text included
  • references/advanced-patterns.mdFull text included

This report is for the version above. We read the available code and instructions without running the skill or checking extra packages it installs. This is not a promise of safety: a different version or setup may behave differently.

  • SKILL.mdInstructions
  • references/advanced-patterns.mdSupporting file
  • references/details.mdSupporting file

Operations mentioned in code and instructions

Read files
SKILL.md:23In the instructionsOpen original file
Detailed pattern documentation lives in `references/details.md`. Read that file when the navigation tier above is insufficient.
Read keys or account settings
references/advanced-patterns.md:50In the instructionsOpen original file
    new Pool({      host: process.env.DB_HOST,      port: parseInt(process.env.DB_PORT || "5432"),
references/advanced-patterns.md:51In the instructionsOpen original file
      host: process.env.DB_HOST,      port: parseInt(process.env.DB_PORT || "5432"),      database: process.env.DB_NAME,
references/advanced-patterns.md:52In the instructionsOpen original file
      port: parseInt(process.env.DB_PORT || "5432"),      database: process.env.DB_NAME,      user: process.env.DB_USER,
Lines read
1,077
File checksum (to compare versions)
51c768fb943a22ce66759f2d14757f4894dd13df14b5b45504db3e22eae17973